0fcb77b991
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
239 lines
6.9 KiB
TypeScript
239 lines
6.9 KiB
TypeScript
/**
|
||
* 生成进度弹窗 — 任务 2.17
|
||
* 三阶段 UI:setup(配置)→ progress(进度轮询)→ completed / failed(结果)
|
||
* V21 设计系统,CSS 类名前缀 ep-gen-
|
||
*/
|
||
import React, { useEffect, useRef } from "react"
|
||
import { Modal, Button } from "@/components/ui"
|
||
import type { TaskItem } from "@/api/tasks"
|
||
|
||
/* ──────────── 类型 ──────────── */
|
||
|
||
export type GenPhase = "setup" | "progress" | "completed" | "failed"
|
||
|
||
export interface GenerationProgressModalProps {
|
||
open: boolean
|
||
phase: GenPhase
|
||
|
||
/* setup 阶段 */
|
||
voiceoverDuration: number | null
|
||
estimatedDuration: number
|
||
onDurationChange: (v: number | null) => void
|
||
onGenerate: () => void
|
||
|
||
/* progress / 结果阶段 */
|
||
task: TaskItem | null
|
||
|
||
/* 通用 */
|
||
submitting: boolean
|
||
onCancel: () => void
|
||
onRetry?: () => void
|
||
onClose?: () => void
|
||
}
|
||
|
||
/* ──────────── 步骤文案映射 ──────────── */
|
||
|
||
const STEP_LABELS: Record<string, string> = {
|
||
queued: "排队中…",
|
||
preparing: "准备素材…",
|
||
generating_video: "渲染视频中…",
|
||
adding_effects: "添加特效…",
|
||
composing: "合成中…",
|
||
encoding: "编码输出中…",
|
||
completed: "生成完成!",
|
||
failed: "生成失败",
|
||
}
|
||
|
||
const getStepLabel = (step: string) => STEP_LABELS[step] || step.replace(/_/g, " ")
|
||
|
||
/* ──────────── 状态徽标颜色 ──────────── */
|
||
|
||
const STATUS_COLOR: Record<string, string> = {
|
||
queued: "#6b7280",
|
||
pending: "#6b7280",
|
||
preparing: "#f59e0b",
|
||
generating_video: "#4f46e5",
|
||
adding_effects: "#7c3aed",
|
||
composing: "#2563eb",
|
||
encoding: "#0891b2",
|
||
completed: "#10b981",
|
||
failed: "#ef4444",
|
||
}
|
||
|
||
/* ──────────── 组件 ──────────── */
|
||
|
||
const GenerationProgressModal: React.FC<GenerationProgressModalProps> = ({
|
||
open,
|
||
phase,
|
||
voiceoverDuration,
|
||
estimatedDuration,
|
||
onDurationChange,
|
||
onGenerate,
|
||
task,
|
||
submitting,
|
||
onCancel,
|
||
onRetry,
|
||
onClose,
|
||
}) => {
|
||
/* 关闭弹窗时重置(避免下次打开残留旧状态) */
|
||
const prevOpen = useRef(false)
|
||
useEffect(() => {
|
||
if (prevOpen.current && !open) {
|
||
/* modal just closed — parent handles reset */
|
||
}
|
||
prevOpen.current = open
|
||
}, [open])
|
||
|
||
const progress = task?.progress ?? 0
|
||
const status = task?.status ?? ""
|
||
const currentStep = task?.current_step ?? ""
|
||
const userMessage = task?.user_message ?? ""
|
||
const errorMessage = task?.error_message ?? ""
|
||
const retryable = task?.retryable ?? false
|
||
|
||
/* ── setup 阶段 ── */
|
||
if (phase === "setup") {
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
title="使用模板生成视频"
|
||
confirmLoading={submitting}
|
||
onOk={onGenerate}
|
||
onCancel={onCancel}
|
||
okText="开始生成"
|
||
cancelText="取消"
|
||
width={440}
|
||
>
|
||
<div className="ep-gen-setup">
|
||
<label className="ep-gen-field-label">配音时长(秒)</label>
|
||
<input
|
||
type="number"
|
||
className="ep-gen-duration-input"
|
||
placeholder="请输入配音时长"
|
||
value={voiceoverDuration ?? ""}
|
||
onChange={(e) => {
|
||
const v = e.target.value ? Number(e.target.value) : null
|
||
onDurationChange(v)
|
||
}}
|
||
min={1}
|
||
max={600}
|
||
/>
|
||
<div className="ep-gen-estimate">
|
||
预估总时长:<strong>{estimatedDuration}s</strong>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
/* ── progress 阶段 ── */
|
||
if (phase === "progress") {
|
||
const stepColor = STATUS_COLOR[status] || STATUS_COLOR[currentStep] || "#4f46e5"
|
||
|
||
return (
|
||
<Modal open={open} title="视频生成中" footer={null} onCancel={onCancel} closable width={480}>
|
||
<div className="ep-gen-progress">
|
||
{/* 进度环 */}
|
||
<div className="ep-gen-progress-ring-wrap">
|
||
<svg className="ep-gen-progress-ring" viewBox="0 0 120 120">
|
||
<circle className="ep-gen-progress-ring-bg" cx="60" cy="60" r="52" />
|
||
<circle
|
||
className="ep-gen-progress-ring-fill"
|
||
cx="60"
|
||
cy="60"
|
||
r="52"
|
||
style={{
|
||
strokeDasharray: `${2 * Math.PI * 52}`,
|
||
strokeDashoffset: `${2 * Math.PI * 52 * (1 - progress / 100)}`,
|
||
stroke: stepColor,
|
||
}}
|
||
/>
|
||
</svg>
|
||
<span className="ep-gen-progress-pct" style={{ color: stepColor }}>
|
||
{progress}%
|
||
</span>
|
||
</div>
|
||
|
||
{/* 当前步骤 */}
|
||
<div className="ep-gen-step-text">
|
||
{userMessage || getStepLabel(currentStep) || "处理中…"}
|
||
</div>
|
||
|
||
{/* 进度条 */}
|
||
<div className="ep-gen-progress-bar">
|
||
<div
|
||
className="ep-gen-progress-bar-fill"
|
||
style={{
|
||
width: `${progress}%`,
|
||
backgroundColor: stepColor,
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* 任务 ID */}
|
||
{task?.id && <div className="ep-gen-task-id">任务 ID: {task.id}</div>}
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
/* ── completed 阶段 ── */
|
||
if (phase === "completed") {
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
title="✅ 生成完成"
|
||
footer={null}
|
||
onCancel={onClose || onCancel}
|
||
closable
|
||
width={440}
|
||
>
|
||
<div className="ep-gen-result">
|
||
<div className="ep-gen-result-icon">🎉</div>
|
||
<div className="ep-gen-result-title">视频生成完成!</div>
|
||
{userMessage && <div className="ep-gen-result-msg">{userMessage}</div>}
|
||
<div className="ep-gen-result-actions">
|
||
<Button buttonType="primary" onClick={onClose || onCancel}>
|
||
查看结果
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
/* ── failed 阶段 ── */
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
title="❌ 生成失败"
|
||
footer={null}
|
||
onCancel={onClose || onCancel}
|
||
closable
|
||
width={440}
|
||
>
|
||
<div className="ep-gen-result ep-gen-result--error">
|
||
<div className="ep-gen-result-icon">😥</div>
|
||
<div className="ep-gen-result-title">视频生成失败</div>
|
||
{(errorMessage || userMessage) && (
|
||
<div className="ep-gen-result-msg ep-gen-result-msg--error">
|
||
{errorMessage || userMessage}
|
||
</div>
|
||
)}
|
||
<div className="ep-gen-result-actions">
|
||
{retryable && onRetry && (
|
||
<Button buttonType="primary" onClick={onRetry}>
|
||
🔄 重试
|
||
</Button>
|
||
)}
|
||
<Button buttonType="secondary" onClick={onClose || onCancel}>
|
||
关闭
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
export default GenerationProgressModal
|