From 41a0724184cce1167b1c69247fe4e080c8485240 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 27 Sep 2026 11:12:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(generate):=20Step6=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=B0=81=E9=9D=A2=E8=B5=B0=20OSS=20+=20?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E6=A0=B7=E5=BC=8F=E6=81=A2=E5=A4=8D=20Modal?= =?UTF-8?q?=20=E5=BC=B9=E7=AA=97=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step6CoverSettings: 单视频本地上传封面时,使用 uploadAssetDirect 实际上传到图片素材库 OSS,拿到真实 URL 再 set;上传期间按钮 loading;原 useEffect 把 setOnUploadFile 置为 null 改为注册 uploadLocalCover 处理函数 - GeneratePage.handleFinish: 增加 blob: URL 兜底过滤(blob: URL 是浏览器本地临时地址,后端无法下载,此时不传 cover_url 让后端回退自动截帧封面,避免 400 保存失败);custom_title 空串不传 undefined - 新增 TitleStyleModal 组件:标题样式改为弹窗模式(参考封面模板/字幕模板 Modal),内置「爆款预设」+「自定义」两个 Tab,底部取消/保存按钮,保存才提交草稿到父组件 - Step4TitleSettings: 移除内联 TitleStylePanel,改为「标题样式」按钮 + Modal 触发入口 Closes: Step6「完成」按钮保存失败 + 标题设置内联铺参数回归 --- apps/web/src/pages/generate/GeneratePage.tsx | 13 +- .../components/Step4TitleSettings.tsx | 57 +++--- .../components/Step6CoverSettings.tsx | 85 ++++++-- .../components/title/TitleStyleModal.css | 90 +++++++++ .../components/title/TitleStyleModal.tsx | 189 ++++++++++++++++++ 5 files changed, 381 insertions(+), 53 deletions(-) create mode 100644 apps/web/src/pages/generate/components/title/TitleStyleModal.css create mode 100644 apps/web/src/pages/generate/components/title/TitleStyleModal.tsx diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 002eb241b..4890343ff 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -441,19 +441,24 @@ const GeneratePage: React.FC = () => { await Promise.all( batchTasks.map(async (task) => { const vi = task.variantIndex - const coverUrl = previewCovers[vi] || "" + const rawCoverUrl = previewCovers[vi] || "" + const coverUrl = rawCoverUrl.startsWith("blob:") ? "" : rawCoverUrl const title = previewTitles[vi] || titleSettings.title || "" return finalizeGeneration(task.taskId, { cover_url: coverUrl || undefined, - custom_title: title, + custom_title: title || undefined, }) }), ) } else if (singleTaskId) { - const coverUrl = coverSettings.thumbnail_url || coverSettings.upload_url || "" + // 单视频:cover_url 仅在非 blob: 本地预览地址时才传;blob: URL 浏览器本地临时地址, + // 后端无法下载,此时不传让后端回退自动截帧封面(避免 400 保存失败)。 + // 正常流程本地上传完成后 uploadLocalCover 会把 URL 替换为 OSS 真实 URL,这里仅兜底异常场景。 + const rawCoverUrl = coverSettings.thumbnail_url || coverSettings.upload_url || "" + const coverUrl = rawCoverUrl.startsWith("blob:") ? "" : rawCoverUrl await finalizeGeneration(singleTaskId, { cover_url: coverUrl || undefined, - custom_title: titleSettings.title || "", + custom_title: titleSettings.title || undefined, }) } else { console.warn("[handleFinish] 未找到任务 ID,跳过 finalize 直接跳转") diff --git a/apps/web/src/pages/generate/components/Step4TitleSettings.tsx b/apps/web/src/pages/generate/components/Step4TitleSettings.tsx index 0beb3a5bb..d3788b4d3 100644 --- a/apps/web/src/pages/generate/components/Step4TitleSettings.tsx +++ b/apps/web/src/pages/generate/components/Step4TitleSettings.tsx @@ -6,13 +6,12 @@ * - 批量:N 个独立标题输入框(AutoComplete 支持标题库选择) * - 标题样式(字体/颜色/位置/大小/粗斜描边/预设):全局统一 */ -import React, { useMemo } from "react" +import React, { useMemo, useState } from "react" import type { TitleSettings } from "../types" -import { POSITION_OPTIONS } from "../constants" -import { FONT_OPTIONS } from "@/components/title/constants" import { useStep4Title } from "../hooks/useStep4Title" import TitleLibraryAutoComplete from "./title/TitleLibraryAutoComplete" -import TitleStylePanel from "./title/TitleStylePanel" +import TitleStyleModal from "./title/TitleStyleModal" +import Button from "@/components/ui/Button" import type { TitleTemplate } from "@/components/title/template-types" interface Step4TitleSettingsProps { @@ -58,13 +57,6 @@ interface Step4TitleSettingsProps { const Step4TitleSettings: React.FC = (props) => { const t = useStep4Title(props) const { - onUpdatePosition, - onUpdateFont, - onUpdateSize, - onToggleBold, - onToggleItalic, - onToggleStroke, - onToggleShadow, onApplyPreset, onUpdateStyle, activePreset, @@ -72,14 +64,12 @@ const Step4TitleSettings: React.FC = (props) => { previewCount = 1, previewTitles, onPreviewTitlesChange, - enableTemplates, - selectedTemplateId, - onApplyTemplate, onConfirmGenerate, generating, selectedCount = 1, } = props + const [showTitleStyleModal, setShowTitleStyleModal] = useState(false) const isBatch = previewCount > 1 /** 更新单个变体标题;变体0同步写回 titleSettings.title(全局样式面板/草稿/TTS 链路依赖) */ @@ -184,27 +174,30 @@ const Step4TitleSettings: React.FC = (props) => { )} - {/* 标题样式面板(全局共用) */} - setShowTitleStyleModal(false)} + titleText={previewTitles?.[0] ?? t.titleSettings.title} settings={t.titleSettings} - onUpdatePosition={onUpdatePosition} - onUpdateFont={onUpdateFont} - onUpdateSize={onUpdateSize} - onToggleBold={onToggleBold} - onToggleItalic={onToggleItalic} - onToggleStroke={onToggleStroke} - onToggleShadow={onToggleShadow} - onApplyPreset={onApplyPreset} - onUpdateStyle={onUpdateStyle} - showCoverToggle activePreset={activePreset} - titlePresets={titlePresets} - POSITION_OPTIONS={POSITION_OPTIONS} - FONT_OPTIONS={FONT_OPTIONS} - enableTemplates={enableTemplates} - selectedTemplateId={selectedTemplateId} - onApplyTemplate={onApplyTemplate} + onSave={(nextSettings, nextPreset) => { + onUpdateStyle?.(nextSettings) + if (nextPreset) onApplyPreset(nextPreset) + }} /> +
+ + +
) } diff --git a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx index 2a24287ca..f20fafd6b 100755 --- a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx +++ b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx @@ -5,8 +5,8 @@ * * 模板 CRUD + 编辑器弹窗 + 自动生成 + 上传 复用 components/cover/useSharedCover */ -import React, { useEffect, useMemo } from "react" -import { Modal, Spin } from "antd" +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { Modal, Spin, message } from "antd" import { LoadingOutlined } from "@ant-design/icons" import type { CoverConfig } from "../types/cover" import type { GeneratedVideo } from "@/api/template-editor" @@ -17,6 +17,7 @@ import CoverSettingsModal from "./cover-settings/CoverSettingsModal" import CoverEditorModal from "./cover-settings/CoverEditorModal" import { useSharedCover } from "@/components/cover/useSharedCover" import { generateCover as apiGenerateCover } from "@/api/generation" +import { uploadAssetDirect, getAssetLibraries } from "@/api/assets" interface Step6CoverSettingsProps { coverSettings: CoverConfig @@ -159,9 +160,65 @@ const Step6CoverSettings: React.FC = (props) => { } }, [isBatch, shared.selectedTemplateId, parentSelectedTemplate, onTemplateChange]) + /** 单视频本地上传封面:选完文件后上传到素材库 OSS,拿到真实 URL 再 set */ + const [uploadingLocalCover, setUploadingLocalCover] = useState(false) + const { coverSettings: curCoverSettings, onCoverSettingsChange } = props + const uploadLocalCover = useCallback( + async (file: File): Promise => { + const hide = message.loading("正在上传封面...", 0) + setUploadingLocalCover(true) + try { + // 立即创建 blob URL 用于即时预览,同时异步上传 OSS + const previewUrl = URL.createObjectURL(file) + onCoverSettingsChange({ + ...curCoverSettings, + upload_url: previewUrl, + thumbnail_url: previewUrl, + mode: "upload", + }) + // 查找图片素材库(复用批量封面的逻辑) + const libs = await getAssetLibraries() + const imageLib = libs.find((l) => l.kind === "image") || libs[0] + if (!imageLib) { + hide() + message.error("未找到素材库,请先创建图片素材库") + return previewUrl + } + const result = await uploadAssetDirect({ file, library_id: imageLib.id }) + const realUrl = result?.url || "" + if (!realUrl) { + hide() + message.warning("上传完成但未获取到URL,将使用本地预览") + return previewUrl + } + hide() + // 替换 blob URL 为真实 OSS URL(blob 用于预览过渡,finalize 时必须用真实 URL) + onCoverSettingsChange({ + ...curCoverSettings, + upload_url: realUrl, + thumbnail_url: realUrl, + mode: "upload", + }) + message.success("封面上传成功") + return realUrl + } catch (err) { + hide() + console.error("[Step6] 封面上传失败:", err) + message.error("封面上传失败,请重试") + return null + } finally { + setUploadingLocalCover(false) + } + }, + [curCoverSettings, onCoverSettingsChange], + ) useEffect(() => { - shared.setOnUploadFile(() => null) - }, [shared]) + // 单视频:注册实际上传函数;批量场景已由 batchCovers.uploadOne 接管, + // 这里不要覆盖(批量时 input ref 绑定到 batchUploadRef,不走 shared.handleFileInputChange) + if (!isBatch) { + shared.setOnUploadFile((file) => uploadLocalCover(file)) + } + }, [shared, isBatch, uploadLocalCover]) const batchUploadRef = React.useRef(null) const [batchUploadCard, setBatchUploadCard] = React.useState(null) @@ -399,7 +456,12 @@ const Step6CoverSettings: React.FC = (props) => { - = (props) => { type="file" accept="image/*" style={{ display: "none" }} - onChange={(e) => { - const file = e.target.files?.[0] - e.target.value = "" - if (!file) return - const url = URL.createObjectURL(file) - props.onCoverSettingsChange({ - ...props.coverSettings, - upload_url: url, - thumbnail_url: url, - mode: "upload", - }) - }} + onChange={shared.handleFileInputChange} /> void + /** 当前标题文字(用于预览) */ + titleText?: string + /** 当前生效的标题样式 */ + settings: TitleSettings + /** 保存回调:把编辑后的 settings 回传父组件 */ + onSave: (settings: TitleSettings, presetKey: string | null) => void + /** 当前选中的爆款预设 key(用于高亮) */ + activePreset?: string | null +} + +/** 把预设应用到 base settings(参考 useTitleStyleUpdaters.applyPreset 逻辑) */ +function applyPresetToSettings(base: TitleSettings, presetKey: string): TitleSettings { + const preset = TITLE_PRESETS_DATA.find((p) => p.key === presetKey) + if (!preset) return base + return { + ...base, + ...(titleStyleConfigToCamel(preset.style) as Partial), + lineOverrides: [], + } +} + +const TitleStyleModal: React.FC = ({ + open, + onClose, + titleText = "标题", + settings, + onSave, + activePreset: externalActivePreset = null, +}) => { + // 弹窗内维护一份草稿 settings,点保存才提交到父组件;取消/关闭丢弃 + const [draft, setDraft] = useState(settings) + const [activePreset, setActivePreset] = useState(externalActivePreset) + + // 每次打开弹窗,用父组件最新 settings 重置草稿 + useEffect(() => { + if (open) { + setDraft(settings) + setActivePreset(externalActivePreset) + } + }, [open, settings, externalActivePreset]) + + const handleApplyPreset = (presetKey: string) => { + setActivePreset(presetKey) + const next = applyPresetToSettings(draft, presetKey) + setDraft(next) + } + + const handleUpdateStyle = (patch: Partial) => { + setDraft((prev) => ({ ...prev, ...patch })) + // 手动调了参数,清除预设高亮(因为不再严格匹配某个预设) + setActivePreset(null) + } + + const handleUpdatePosition = (position: string) => + handleUpdateStyle({ position: position as TitleSettings["position"] }) + const handleUpdateFont = (font: string) => handleUpdateStyle({ font }) + const handleUpdateSize = (size: number) => handleUpdateStyle({ size }) + const handleToggleBold = () => handleUpdateStyle({ bold: !draft.bold }) + const handleToggleItalic = () => handleUpdateStyle({ italic: !draft.italic }) + const handleToggleStroke = () => handleUpdateStyle({ stroke: !draft.stroke }) + const handleToggleShadow = () => handleUpdateStyle({ shadow: !draft.shadow }) + + const handleSave = () => { + onSave(draft, activePreset) + onClose() + } + + return ( + +
+ {/* 顶部预览 */} +
+ +
+ + + {TITLE_PRESETS_DATA.map((p) => { + const previewSettings = applyPresetToSettings(draft, p.key) + const isActive = activePreset === p.key + return ( + + ) + })} +
+ ), + }, + { + key: "custom", + label: "自定义", + children: ( +
+ +
+ ), + }, + ]} + /> + + + {/* 底部按钮栏 */} +
+ + +
+
+ ) +} + +export default TitleStyleModal