6ce40d76ea
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 186h43m22s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 186h43m25s
Deploy / Deploy Staging (push) Failing after 186h43m53s
CI/CD Pipeline / Frontend Lint (push) Failing after 186h44m20s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 186h44m24s
- 新增 GenerationProgressModal 组件:三阶段 UI(setup → progress → completed/failed) - 集成 React Query 轮询:refetchInterval 2s 轮询任务状态,终态自动停止 - 进度展示:SVG 进度环 + 进度条 + 步骤文案 + 任务 ID - 失败处理:错误信息展示 + 可重试时显示重试按钮 - 对接 createGenerationTask / getTask / retryTask API - V21 设计系统,CSS 类名前缀 ep-gen-
252 lines
7.0 KiB
TypeScript
252 lines
7.0 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;
|