Compare commits

...

2 Commits

Author SHA1 Message Date
xiaoxia a3e885a595 fix(generate): 标题模板切换为内联大卡片网格模式 + 撤销错误Modal方案
- GeneratePage: enableTemplates 从 false 改为 true,启用 TitleStylePanel 的 ttv3 大卡片模板网格模式
- Step4TitleSettings: 从 develop 恢复原版(直接渲染 TitleStylePanel,enableTemplates=true 时展示大卡片网格+编辑/复制/删除+系统标签+紫色高亮选中+右上角「新建模板」按钮+TitleTemplateEditor编辑弹窗),撤销上一版错误的 TitleStyleModal 弹窗入口方案
- 删除上一版错误新增的 TitleStyleModal.tsx / TitleStyleModal.css(错误方向:选样式走弹窗;正确方向:大卡片网格内嵌,仅编辑/新建时弹窗)
- 保留:Step6 封面本地上传 OSS 直传修复 + handleFinish blob: URL 防御兜底(这部分是 P0 正确修复)

Closes: 标题样式应该是内联大卡片网格而非弹窗选样式(9/23 已做对,PR#2054改字体时enableTemplates被置为false导致UI退化为旧预设+参数Tab)
2026-09-27 11:41:05 +08:00
xiaoxia 41a0724184 fix(generate): Step6 本地上传封面走 OSS + 标题样式恢复 Modal 弹窗模式
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m36s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m42s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m5s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m54s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m18s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m3s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 30s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 30s
AI Code Review / AI Code Review (pull_request) Successful in 6m29s
- 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「完成」按钮保存失败 + 标题设置内联铺参数回归
2026-09-27 11:12:54 +08:00
2 changed files with 79 additions and 23 deletions
+11 -6
View File
@@ -137,8 +137,8 @@ const GeneratePage: React.FC = () => {
}
}, [selectedVoice, isBatch, voiceModePerVideo, setVoiceLibraryIds])
/* ── 标题面板模式:默认展示样式参数(false);需要大卡片模板网格时再切 true ── */
const enableTemplates = false
/* ── 标题面板模式:true = 内联大卡片模板网格(默认),false = 旧预设+参数 Tab ── */
const enableTemplates = true
/* ── Step5 保存中状态 ── */
const [finishing, setFinishing] = useState(false)
@@ -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 直接跳转")
@@ -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<Step6CoverSettingsProps> = (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<string | null> => {
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<HTMLInputElement>(null)
const [batchUploadCard, setBatchUploadCard] = React.useState<number | null>(null)
@@ -399,7 +456,12 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<Button buttonType="ghost" onClick={() => shared.setShowCoverSettings(true)}>
⚙️ 封面模板
</Button>
<Button buttonType="ghost" onClick={shared.handleUploadClick}>
<Button
buttonType="ghost"
onClick={shared.handleUploadClick}
disabled={uploadingLocalCover}
loading={uploadingLocalCover}
>
📷 本地上传
</Button>
<input
@@ -407,18 +469,7 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (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}
/>
<input
ref={batchUploadRef}