Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dd259c550 |
@@ -21,6 +21,7 @@ import GenerateStepActions from "./components/GenerateStepActions"
|
||||
import { useGenerateFormState } from "./hooks/useGenerateFormState"
|
||||
import { useStepNavigation } from "./hooks/useStepNavigation"
|
||||
import { useGenerateVideo } from "./hooks/useGenerateVideo"
|
||||
import { confirmGeneration } from "@/api/generation/confirm"
|
||||
|
||||
import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans"
|
||||
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
|
||||
@@ -139,6 +140,9 @@ const GeneratePage: React.FC = () => {
|
||||
/* ── 数量选择弹窗 ── */
|
||||
const [countModalOpen, setCountModalOpen] = useState(false)
|
||||
|
||||
/* ── Step5 保存中状态 ── */
|
||||
const [finishing, setFinishing] = useState(false)
|
||||
|
||||
/* ── #1970 流程重构:分支弹窗 ── */
|
||||
const [voiceModalOpen, setVoiceModalOpen] = useState(false)
|
||||
const [scriptModalOpen, setScriptModalOpen] = useState(false)
|
||||
@@ -404,9 +408,81 @@ const GeneratePage: React.FC = () => {
|
||||
},
|
||||
})
|
||||
|
||||
/* ── 最终成片 ── */
|
||||
/* ── 最终成片(单视频) ── */
|
||||
const finalVideo = generatedVideos[0]
|
||||
|
||||
/* ── Step5 完成:调用 confirm 入库 + 跳转 ── */
|
||||
const handleFinish = useCallback(async () => {
|
||||
if (finishing) return
|
||||
// 校验:单视频必须已生成;批量必须所有已选视频有封面或确认跳过
|
||||
if (isBatch) {
|
||||
if (generatedVideos.length === 0) {
|
||||
message.warning("请等待视频生成完成")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!finalVideo) {
|
||||
message.warning("请等待视频生成完成")
|
||||
return
|
||||
}
|
||||
}
|
||||
setFinishing(true)
|
||||
const hide = message.loading("正在保存到视频库...", 0)
|
||||
try {
|
||||
const taskIds =
|
||||
batchTasks && batchTasks.length > 0
|
||||
? batchTasks.map((t) => t.taskId).filter(Boolean)
|
||||
: finalVideo?.generation_task_id
|
||||
? [finalVideo.generation_task_id]
|
||||
: []
|
||||
|
||||
// 单视频/批量:为每个任务调用 confirm(传入封面)
|
||||
if (isBatch && previewCovers.length > 0) {
|
||||
await Promise.all(
|
||||
taskIds.map(async (taskId, idx) => {
|
||||
const coverUrl = previewCovers[idx] || ""
|
||||
return confirmGeneration(taskId, {
|
||||
cover_url: coverUrl || undefined,
|
||||
custom_title: previewTitles[idx] || titleSettings.title || "",
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else if (finalVideo?.generation_task_id) {
|
||||
const coverUrl = coverSettings.thumbnail_url || coverSettings.upload_url || ""
|
||||
await confirmGeneration(finalVideo.generation_task_id, {
|
||||
cover_url: coverUrl || undefined,
|
||||
custom_title: titleSettings.title || "",
|
||||
})
|
||||
}
|
||||
hide()
|
||||
message.success("已保存到视频库")
|
||||
navigate("/app/products")
|
||||
} catch (err) {
|
||||
hide()
|
||||
console.error("[保存失败]", err)
|
||||
const detail =
|
||||
(err as { response?: { data?: { detail?: string; message?: string } } })?.response?.data
|
||||
?.detail ||
|
||||
(err as { response?: { data?: { detail?: string; message?: string } } })?.response?.data
|
||||
?.message ||
|
||||
"保存失败,请稍后在任务历史查看"
|
||||
message.error(detail)
|
||||
} finally {
|
||||
setFinishing(false)
|
||||
}
|
||||
}, [
|
||||
finishing,
|
||||
isBatch,
|
||||
finalVideo,
|
||||
generatedVideos,
|
||||
batchTasks,
|
||||
previewCovers,
|
||||
previewTitles,
|
||||
titleSettings.title,
|
||||
coverSettings,
|
||||
navigate,
|
||||
])
|
||||
|
||||
/* ── 布局 class ── */
|
||||
const layoutClassName = "xx-generate-layout full-width"
|
||||
|
||||
@@ -480,9 +556,6 @@ const GeneratePage: React.FC = () => {
|
||||
onPreviewCountChange={setPreviewCount}
|
||||
videoRatio={videoRatio as "9:16" | "16:9"}
|
||||
onVideoRatioChange={(r) => setVideoRatio(r)}
|
||||
selectedScript={selectedScript}
|
||||
ttsVoiceId={ttsVoiceId}
|
||||
ttsVoiceSource={ttsVoiceSource}
|
||||
onSelectedVoiceChange={setSelectedVoice}
|
||||
generating={generating}
|
||||
generated={generated}
|
||||
@@ -565,12 +638,6 @@ const GeneratePage: React.FC = () => {
|
||||
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
|
||||
🔗 分享
|
||||
</button>
|
||||
<button
|
||||
className="xx-btn xx-btn-ghost xx-btn-sm"
|
||||
onClick={() => navigate("/app/products")}
|
||||
>
|
||||
📁 前往成片库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,6 +657,8 @@ const GeneratePage: React.FC = () => {
|
||||
pointsInsufficient={insufficientPoints}
|
||||
insufficientReason={pointsEstimate.reason}
|
||||
onRecharge={() => navigate("/app/points/recharge")}
|
||||
onFinish={handleFinish}
|
||||
finishing={finishing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 步骤 1~3:上一步 / 下一步
|
||||
* 步骤 4(确认生成/进度):未开始 →「✨ 确认生成视频」;生成中 →「⏳ 视频渲染中…」;
|
||||
* 失败 →「🔄 重新生成」;全部完成 →「下一步:选择封面 →」
|
||||
* 步骤 5(选择封面):仅上一步,无主按钮
|
||||
* 步骤 5(选择封面):上一步 + 完成按钮
|
||||
*/
|
||||
import React from "react"
|
||||
import { Tooltip } from "antd"
|
||||
@@ -32,6 +32,10 @@ export interface GenerateStepActionsProps {
|
||||
freeClipsUsedThisTime?: number
|
||||
/** 前往充值 */
|
||||
onRecharge?: () => void
|
||||
/** Step5 点击完成(保存入库并跳转) */
|
||||
onFinish?: () => void | Promise<void>
|
||||
/** Step5 保存中 */
|
||||
finishing?: boolean
|
||||
}
|
||||
|
||||
const GenerateStepActions: React.FC<GenerateStepActionsProps> = ({
|
||||
@@ -48,6 +52,8 @@ const GenerateStepActions: React.FC<GenerateStepActionsProps> = ({
|
||||
insufficientReason,
|
||||
freeClipsUsedThisTime,
|
||||
onRecharge,
|
||||
onFinish,
|
||||
finishing,
|
||||
}) => {
|
||||
const renderPrimaryButton = () => {
|
||||
/* 步骤 1~3:上一步 / 下一步 */
|
||||
@@ -126,8 +132,18 @@ const GenerateStepActions: React.FC<GenerateStepActionsProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
/* 步骤 5(封面,最后一步):无主按钮 */
|
||||
return null
|
||||
/* 步骤 5(封面,最后一步):完成按钮 */
|
||||
return (
|
||||
<button
|
||||
className="xx-btn xx-btn-primary"
|
||||
onClick={() => {
|
||||
if (onFinish && !finishing) void onFinish()
|
||||
}}
|
||||
disabled={finishing}
|
||||
>
|
||||
{finishing ? "⏳ 保存中…" : "✅ 完成"}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import React from "react"
|
||||
import type { CoverConfig } from "../types/cover"
|
||||
import type { TitleSettings } from "../types"
|
||||
import type { ScriptItem } from "@/api/scripts"
|
||||
import Step1EditMode from "./Step1EditMode"
|
||||
import type { EditMode } from "./Step1EditMode"
|
||||
import Step2MaterialSelect from "../components/Step2MaterialSelect"
|
||||
@@ -90,10 +89,6 @@ export interface GenerateStepContentProps {
|
||||
previewCovers: string[]
|
||||
onPreviewCoversChange: (urls: string[]) => void
|
||||
selectedVariantIds?: number[]
|
||||
/* ── 摘要信息(#1970 Step4 展示用) ── */
|
||||
selectedScript: ScriptItem | null
|
||||
ttsVoiceId: string
|
||||
ttsVoiceSource: "preset" | "clone"
|
||||
}
|
||||
|
||||
export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => {
|
||||
@@ -147,9 +142,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
|
||||
previewCovers,
|
||||
onPreviewCoversChange,
|
||||
selectedVariantIds,
|
||||
selectedScript,
|
||||
ttsVoiceId,
|
||||
ttsVoiceSource,
|
||||
} = props
|
||||
|
||||
switch (currentStep) {
|
||||
@@ -208,40 +200,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
|
||||
case 4:
|
||||
return (
|
||||
<div className="xx-form-section">
|
||||
{/* 配置摘要(#1970) */}
|
||||
<div
|
||||
style={{
|
||||
padding: 14,
|
||||
background: "#f9fafb",
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.8,
|
||||
color: "#374151",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 6, color: "#111" }}>
|
||||
📋 生成配置
|
||||
</div>
|
||||
<div>🎬 剪辑模式:{editMode === "random" ? "🎲 随机混剪" : "📖 叙事剪辑"}</div>
|
||||
{editMode === "random" ? (
|
||||
<div>🎙️ 配音来源:配音库音频</div>
|
||||
) : (
|
||||
<>
|
||||
<div>📝 文案:{selectedScript?.title ?? "未选择"}</div>
|
||||
<div>
|
||||
🎙️ 合成配音音色:
|
||||
{ttsVoiceId
|
||||
? `${ttsVoiceSource === "clone" ? "克隆音色" : "系统音色"}(${ttsVoiceId.slice(0, 8)}...)`
|
||||
: "未选择"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>📱 视频比例:{videoRatio}</div>
|
||||
<div>🎯 智能降重:{dedupEnabled ? "已开启" : "已关闭"}</div>
|
||||
{previewCount > 1 && <div>📦 生成数量:{previewCount} 个</div>}
|
||||
</div>
|
||||
|
||||
{previewCount > 1 ? (
|
||||
<BatchGenerationGrid
|
||||
tasks={batchTasks}
|
||||
|
||||
@@ -53,7 +53,7 @@ interface UseBatchCoversOptions {
|
||||
}
|
||||
|
||||
export function useBatchCovers({
|
||||
selectedTemplate,
|
||||
selectedTemplate: _selectedTemplate,
|
||||
generatedVideos,
|
||||
titles,
|
||||
titleStyle,
|
||||
@@ -102,7 +102,7 @@ export function useBatchCovers({
|
||||
addBusy(index)
|
||||
try {
|
||||
const titleText = titles[index] || ""
|
||||
const response = await generateCover(selectedTemplate, {
|
||||
const response = await generateCover("default", {
|
||||
generated_video_id: target.id,
|
||||
video_url: target.file_url || target.download_url || "",
|
||||
cover_type: "ai_frame",
|
||||
@@ -166,7 +166,7 @@ export function useBatchCovers({
|
||||
removeBusy(index)
|
||||
}
|
||||
},
|
||||
[generatedVideos, titles, titleStyle, selectedTemplate, patchCover, addBusy, removeBusy],
|
||||
[generatedVideos, titles, titleStyle, patchCover, addBusy, removeBusy],
|
||||
)
|
||||
|
||||
/** 为第 index 个视频上传自定义封面 */
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
interface UseStep6CoverProps {
|
||||
coverSettings: CoverConfig
|
||||
onCoverSettingsChange: (settings: CoverConfig) => void
|
||||
/** 当前选中的模板 ID */
|
||||
/** 当前选中的视频模板 ID(封面模板由本地 state 管理) */
|
||||
selectedTemplate?: string
|
||||
/** Step4 标题设置,用于封面叠加标题 */
|
||||
titleSettings?: TitleSettings
|
||||
@@ -30,7 +30,7 @@ interface UseStep6CoverProps {
|
||||
export function useStep6Cover({
|
||||
coverSettings,
|
||||
onCoverSettingsChange,
|
||||
selectedTemplate = "",
|
||||
selectedTemplate: _selectedTemplate = "",
|
||||
titleSettings,
|
||||
generatedVideos,
|
||||
}: UseStep6CoverProps) {
|
||||
@@ -79,7 +79,8 @@ export function useStep6Cover({
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedTemplate) {
|
||||
const activeCoverTemplateId = selectedTemplateId || "default"
|
||||
if (!activeCoverTemplateId) {
|
||||
message.error("请先选择模板")
|
||||
return
|
||||
}
|
||||
@@ -95,7 +96,7 @@ export function useStep6Cover({
|
||||
}, 300000)
|
||||
|
||||
try {
|
||||
const response = await generateCover(selectedTemplate, {
|
||||
const response = await generateCover(activeCoverTemplateId, {
|
||||
generated_video_id: finalVideo.id,
|
||||
video_url: finalVideo.file_url || finalVideo.download_url || "",
|
||||
cover_type: "ai_frame",
|
||||
@@ -156,7 +157,7 @@ export function useStep6Cover({
|
||||
setGenerating(false)
|
||||
}
|
||||
}, [
|
||||
selectedTemplate,
|
||||
selectedTemplateId,
|
||||
finalVideo,
|
||||
coverSettings,
|
||||
onCoverSettingsChange,
|
||||
|
||||
Reference in New Issue
Block a user