20fe447efb
- 单视频标题校验与 buildPayload.validateGenerateInputs 对齐:aiAutoSelect 自动模式允许空标题(后端生成,既有设计),手动模式必填,逻辑显式化 - TTS 试听 effect 依赖收窄到 previewTitles[0](variant0Title),编辑其他 变体标题不再触发多余 TTS 请求 - BatchGenerationGrid 排序加 (variantIndex || 0) 兜底防 NaN - CanvasPreviewGrid 渲染数加 MAX_PREVIEW_COUNT(10) 硬上限,防媒体元素过多卡顿
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
/**
|
||
* 第5步「确认生成」— 批量渲染进度网格(Issue #1677)
|
||
*
|
||
* N 个正式生成任务各自独立卡片:进度条 / 成功成片播放 / 失败原因 + 单独重试。
|
||
* 数据来自 useGenerateVideo 的 batchTasks(useGenerationPolling 实时回传)。
|
||
*/
|
||
import React from "react"
|
||
import { LoadingOutlined, CheckCircleFilled, CloseCircleOutlined } from "@ant-design/icons"
|
||
import type { BatchTaskState } from "../hooks/generate-video/useGenerationPolling"
|
||
import type { GeneratedVideo } from "@/api/template-editor"
|
||
|
||
interface BatchGenerationGridProps {
|
||
tasks: BatchTaskState[]
|
||
/** 变体标题(按变体序号取) */
|
||
titles: string[]
|
||
/** 失败任务重试 */
|
||
onRetryTask: (taskId: string) => void
|
||
}
|
||
|
||
const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
|
||
tasks,
|
||
titles,
|
||
onRetryTask,
|
||
}) => {
|
||
const sorted = [...tasks].sort((a, b) => (a.variantIndex || 0) - (b.variantIndex || 0))
|
||
|
||
return (
|
||
<div className="xx-form-section">
|
||
<div className="xx-preview-header">
|
||
<h3>🎬 正在生成 {tasks.length} 个视频</h3>
|
||
<span style={{ fontSize: 13, color: "var(--text-secondary, #666)" }}>
|
||
完成 {tasks.filter((t) => t.status === "completed").length} / {tasks.length}
|
||
</span>
|
||
</div>
|
||
<div className="xx-batch-gen-grid">
|
||
{sorted.map((task) => {
|
||
const title = titles[task.variantIndex] || `视频 ${task.variantIndex + 1}`
|
||
const video = (task.videos?.[0] || null) as GeneratedVideo | null
|
||
return (
|
||
<div key={task.taskId} className={`xx-batch-gen-card status-${task.status}`}>
|
||
<div className="xx-batch-gen-card-head">
|
||
<span className="xx-batch-gen-card-title" title={title}>
|
||
{task.status === "completed" ? (
|
||
<CheckCircleFilled style={{ color: "#52c41a", marginRight: 6 }} />
|
||
) : task.status === "failed" ? (
|
||
<CloseCircleOutlined style={{ color: "#ef4444", marginRight: 6 }} />
|
||
) : (
|
||
<LoadingOutlined style={{ color: "#1677ff", marginRight: 6 }} />
|
||
)}
|
||
视频 {task.variantIndex + 1}:{title}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="xx-batch-gen-card-body">
|
||
{task.status === "running" && (
|
||
<>
|
||
<div className="xx-gen-progress-bar">
|
||
<div
|
||
className="xx-gen-progress-bar-fill"
|
||
style={{ width: `${Math.min(task.progress, 100)}%` }}
|
||
/>
|
||
</div>
|
||
<div className="xx-batch-gen-card-pct">{Math.round(task.progress)}%</div>
|
||
</>
|
||
)}
|
||
{task.status === "completed" && video && (
|
||
<video
|
||
src={video.download_url || video.file_url}
|
||
controls
|
||
style={{ width: "100%", borderRadius: 8, background: "#000", maxHeight: 280 }}
|
||
poster={video.thumbnail_url}
|
||
/>
|
||
)}
|
||
{task.status === "completed" && !video && (
|
||
<div className="xx-batch-gen-card-done">✅ 已完成(成片可在下一步选择封面)</div>
|
||
)}
|
||
{task.status === "failed" && (
|
||
<div className="xx-batch-gen-card-failed">
|
||
<div className="xx-batch-gen-card-err">{task.error || "生成失败"}</div>
|
||
<button
|
||
type="button"
|
||
className="xx-btn xx-btn-primary xx-btn-sm"
|
||
onClick={() => onRetryTask(task.taskId)}
|
||
>
|
||
🔄 重试此视频
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default BatchGenerationGrid
|