a4c008e829
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m31s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m15s
CI/CD Pipeline / Validate - Style (push) Successful in 2m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m36s
CI/CD Pipeline / Unit Tests (push) Failing after 6m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Validate - Security (push) Successful in 9m9s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 191h47m29s
CI/CD Pipeline / Deploy Production (push) Failing after 191h47m29s
CI/CD Pipeline / CI Gate (push) Failing after 191h47m33s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 191h47m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 191h47m33s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 191h55m0s
CI/CD Pipeline / Build Production API Image (push) Failing after 191h47m33s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 191h55m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 191h55m4s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 191h56m40s
CI/CD Pipeline / Frontend Lint (push) Failing after 191h56m40s
CI/CD Pipeline / PR Build Web Image (push) Failing after 191h56m40s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 191h56m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 192h31m24s
135 lines
5.1 KiB
TypeScript
135 lines
5.1 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) => Number(a.variantIndex || 0) - Number(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"
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 320px))",
|
||
justifyContent: "center",
|
||
justifyItems: "center",
|
||
gap: 14,
|
||
width: "100%",
|
||
}}
|
||
>
|
||
{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}`}
|
||
style={{ maxWidth: 320 }}
|
||
>
|
||
<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 && (
|
||
// 竖屏自适应容器(#1750):成片固定 1080×1920(9:16),
|
||
// 视频按真实宽高比 contain 显示,黑底居中,杜绝横屏播放器左右大黑边
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
background: "transparent",
|
||
borderRadius: 8,
|
||
padding: 0,
|
||
}}
|
||
>
|
||
<video
|
||
src={video.download_url || video.file_url}
|
||
controls
|
||
style={{
|
||
width: "100%",
|
||
maxHeight: "70vh",
|
||
aspectRatio: "9 / 16",
|
||
objectFit: "cover",
|
||
borderRadius: 4,
|
||
background: "transparent",
|
||
}}
|
||
poster={video.thumbnail_url}
|
||
/>
|
||
</div>
|
||
)}
|
||
{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
|