Files
xiaoxia-saas/apps/web/src/pages/editing-planner/components/GenerationProgressModal.tsx
T
灵应 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
feat(editing-planner): 实现生成进度展示 — 任务 2.17
- 新增 GenerationProgressModal 组件:三阶段 UI(setup → progress → completed/failed)
- 集成 React Query 轮询:refetchInterval 2s 轮询任务状态,终态自动停止
- 进度展示:SVG 进度环 + 进度条 + 步骤文案 + 任务 ID
- 失败处理:错误信息展示 + 可重试时显示重试按钮
- 对接 createGenerationTask / getTask / retryTask API
- V21 设计系统,CSS 类名前缀 ep-gen-
2026-07-01 18:09:58 +08:00

252 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 生成进度弹窗 — 任务 2.17
* 三阶段 UIsetup(配置)→ 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;