da53b6d175
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
refactor: GeneratePage Phase 1 — 抽离常量/类型/UI组件(3036→2682行)
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
/**
|
||
* 智能剪辑右侧生成结果面板
|
||
*/
|
||
import React from "react"
|
||
import { Typography } from "antd"
|
||
import {
|
||
PlayCircleOutlined,
|
||
CloseCircleOutlined,
|
||
DownloadOutlined,
|
||
ShareAltOutlined,
|
||
} from "@ant-design/icons"
|
||
import type { GeneratedVideo } from "@/api/template-editor"
|
||
import { formatDuration } from "@/api/voice-clone"
|
||
|
||
const { Text } = Typography
|
||
|
||
interface GenerateResultPanelProps {
|
||
/** 是否已生成完成 */
|
||
generated: boolean
|
||
/** 是否正在生成中 */
|
||
generating: boolean
|
||
/** 生成进度(0-100) */
|
||
progress: number
|
||
/** 生成错误信息 */
|
||
generateError: string | null
|
||
/** 生成的视频列表 */
|
||
generatedVideos: GeneratedVideo[]
|
||
/** 点击视频卡片预览回调 */
|
||
onVideoPreview: (video: GeneratedVideo) => void
|
||
/** 下载回调 */
|
||
onDownload: () => void
|
||
/** 分享回调 */
|
||
onShare: () => void
|
||
/** 前往成片库回调 */
|
||
onGoToLibrary: () => void
|
||
}
|
||
|
||
const GenerateResultPanel: React.FC<GenerateResultPanelProps> = ({
|
||
generated,
|
||
generating,
|
||
progress,
|
||
generateError,
|
||
generatedVideos,
|
||
onVideoPreview,
|
||
onDownload,
|
||
onShare,
|
||
onGoToLibrary,
|
||
}) => {
|
||
return (
|
||
<div className="xx-generate-result">
|
||
<div className="xx-result-header">
|
||
<h3>生成结果</h3>
|
||
{generated && generatedVideos.length > 0 && (
|
||
<span className="xx-result-count">{generatedVideos.length} 个视频</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 生成中进度 */}
|
||
{generating && (
|
||
<div className="xx-result-progress">
|
||
<div className="xx-progress-circle">
|
||
<svg viewBox="0 0 80 80">
|
||
<circle
|
||
cx="40"
|
||
cy="40"
|
||
r="36"
|
||
fill="none"
|
||
stroke="var(--border-color)"
|
||
strokeWidth="6"
|
||
/>
|
||
<circle
|
||
cx="40"
|
||
cy="40"
|
||
r="36"
|
||
fill="none"
|
||
stroke="var(--primary-color)"
|
||
strokeWidth="6"
|
||
strokeDasharray={`${Math.round(progress) * 2.26} 226`}
|
||
strokeLinecap="round"
|
||
transform="rotate(-90 40 40)"
|
||
/>
|
||
</svg>
|
||
<span className="xx-progress-percent">{Math.round(progress)}%</span>
|
||
</div>
|
||
<div className="xx-progress-text">
|
||
<Text strong style={{ fontSize: 14, display: "block", marginBottom: 4 }}>
|
||
正在生成视频
|
||
</Text>
|
||
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
|
||
AI 正在处理素材,请稍候…
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 生成失败 */}
|
||
{generateError && !generating && (
|
||
<div className="xx-result-empty">
|
||
<CloseCircleOutlined style={{ fontSize: 40, color: "#ff4d4f", marginBottom: 12 }} />
|
||
<Text strong style={{ display: "block", marginBottom: 4 }}>
|
||
生成失败
|
||
</Text>
|
||
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
|
||
{typeof generateError === "string" ? generateError : "请重试"}
|
||
</Text>
|
||
</div>
|
||
)}
|
||
|
||
{/* 空状态 */}
|
||
{!generated && !generating && !generateError && (
|
||
<div className="xx-result-empty">
|
||
<PlayCircleOutlined
|
||
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
|
||
/>
|
||
<Text style={{ color: "var(--text-secondary)", fontSize: 13 }}>
|
||
完成配置后点击「确认生成」
|
||
</Text>
|
||
<Text style={{ color: "var(--text-tertiary)", fontSize: 12, marginTop: 4 }}>
|
||
生成的视频将在这里展示
|
||
</Text>
|
||
</div>
|
||
)}
|
||
|
||
{/* 生成结果卡片列表 */}
|
||
{generated && generatedVideos.length > 0 && (
|
||
<div className="xx-video-grid">
|
||
{generatedVideos.map((video, idx) => (
|
||
<div
|
||
key={video.id || idx}
|
||
className="xx-video-card"
|
||
onClick={() => onVideoPreview(video)}
|
||
>
|
||
<div className="xx-video-thumb">
|
||
{video.thumbnail_url ? (
|
||
<img src={video.thumbnail_url} alt="" />
|
||
) : (
|
||
<div className="xx-video-thumb-placeholder">
|
||
<PlayCircleOutlined style={{ fontSize: 32, opacity: 0.5 }} />
|
||
</div>
|
||
)}
|
||
<div className="xx-video-play-overlay">
|
||
<PlayCircleOutlined style={{ fontSize: 36, color: "#fff" }} />
|
||
</div>
|
||
{video.duration && (
|
||
<span className="xx-video-duration">{formatDuration(video.duration)}</span>
|
||
)}
|
||
</div>
|
||
<div className="xx-video-info">
|
||
<div className="xx-video-title">视频 {idx + 1}</div>
|
||
<div className="xx-video-actions">
|
||
<button
|
||
className="xx-video-action-btn"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onDownload()
|
||
}}
|
||
>
|
||
<DownloadOutlined />
|
||
</button>
|
||
<button
|
||
className="xx-video-action-btn"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onShare()
|
||
}}
|
||
>
|
||
<ShareAltOutlined />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{generated && (
|
||
<div className="xx-result-footer">
|
||
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onGoToLibrary}>
|
||
前往成片库 →
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default GenerateResultPanel
|