feat(P1): #557 剪辑编辑器改为纯模板编辑器
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 17s
AI Code Review / AI Code Review (pull_request) Failing after 31s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m26s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m27s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1412h10m1s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1412h10m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1412h10m2s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1412h10m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1412h10m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1412h42m23s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1412h42m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1412h42m21s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1412h42m23s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 17s
AI Code Review / AI Code Review (pull_request) Failing after 31s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m26s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m27s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1412h10m1s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1412h10m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1412h10m2s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1412h10m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1412h10m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1412h10m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1412h10m18s
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1412h42m23s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1412h42m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1412h42m21s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1412h42m23s
- 移除编辑器内所有直接生成视频入口(生成按钮、生成进度、生成历史) - 删除相关API调用:generateEditPlan/cancelGeneration/getGenerationStatus等 - 删除GenerationHistoryModal引用 - 编辑器标题改为「模板制作」,面包屑同步更新 - 我的模板页「使用模板生成」改为跳转智能剪辑页面 - 保留模板制作/保存/预览核心功能 - 保留标题/封面只读配置(模板数据的一部分,保存时一并存储)
This commit is contained in:
@@ -59,7 +59,7 @@ const ROUTE_TITLE_MAP: Record<string, string> = {
|
||||
"/app/subscription/upgrade": "升级订阅",
|
||||
"/app/subscription/billing": "账单管理",
|
||||
"/app/profile": "个人设置",
|
||||
"/app/editing-planner": "剪辑规划",
|
||||
"/app/editing-planner": "模板制作",
|
||||
"/app/my-templates": "我的模板",
|
||||
"/app/voice-clone": "我的音色",
|
||||
"/app/voice-materials": "配音库",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* 剪辑计划编辑器 — V8 原型 1:1 还原
|
||||
* 模板编辑器 — 制作/编辑剪辑模板
|
||||
* 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px)
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import { message, Modal, Progress, Button } from "antd"
|
||||
import { message, Modal } from "antd"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import type {
|
||||
EditingTemplate,
|
||||
@@ -21,29 +21,16 @@ import {
|
||||
MODE_LABELS,
|
||||
} from "@/api/editingPlanner"
|
||||
import type {
|
||||
EditPlanGeneration,
|
||||
EditPlanConfig,
|
||||
GeneratedVideo,
|
||||
MediaAsset,
|
||||
TransitionEffect,
|
||||
TitleConfig,
|
||||
} from "@/api/editPlans"
|
||||
import {
|
||||
getMediaAssets,
|
||||
getEditPlanGenerations,
|
||||
getEditPlan,
|
||||
createEditPlan,
|
||||
updateEditPlan,
|
||||
generateEditPlan,
|
||||
getGenerationStatus,
|
||||
getGenerationTaskResults,
|
||||
cancelGeneration,
|
||||
getEditPlanClips,
|
||||
createEditPlanClip,
|
||||
batchDeleteEditPlanClips,
|
||||
type EditPlanClip,
|
||||
type CreateEditPlanClipRequest,
|
||||
type ClipStatusItem,
|
||||
} from "@/api/editPlans"
|
||||
import { useUndoRedo } from "./hooks/useUndoRedo"
|
||||
import type {
|
||||
@@ -97,7 +84,6 @@ import GreenScreenPanel from "./components/GreenScreenPanel"
|
||||
import StickerPanel from "./components/StickerPanel"
|
||||
|
||||
import SaveModal from "./components/SaveModal"
|
||||
import GenerationHistoryModal from "./components/GenerationHistoryModal"
|
||||
import { DEFAULT_BGM_MIX_CONFIG, type BgmMixConfig } from "@/api/bgm"
|
||||
import "./EditingPlanner.css"
|
||||
|
||||
@@ -230,27 +216,9 @@ const EditingPlanner: React.FC = () => {
|
||||
setSelectedAssetIds(ids)
|
||||
}
|
||||
|
||||
/* ── 生成历史 ── */
|
||||
const [genHistoryOpen, setGenHistoryOpen] = useState(false)
|
||||
const [genHistory, setGenHistory] = useState<EditPlanGeneration[]>([])
|
||||
const [genHistoryLoading, setGenHistoryLoading] = useState(false)
|
||||
|
||||
/* ── 剪辑计划(从列表页编辑进入时) ── */
|
||||
const [loadedPlanId, setLoadedPlanId] = useState<string | null>(urlPlanId || null)
|
||||
|
||||
/* ── 生成进度 ── */
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [genProgress, setGenProgress] = useState(0)
|
||||
const [genTotalClips, setGenTotalClips] = useState(0)
|
||||
const [genDoneClips, setGenDoneClips] = useState(0)
|
||||
const [generated, setGenerated] = useState(false)
|
||||
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([])
|
||||
const [genError, setGenError] = useState<string | null>(null)
|
||||
const [cancelling, setCancelling] = useState(false)
|
||||
const [genCancelled, setGenCancelled] = useState(false)
|
||||
const [genClipStatuses, setGenClipStatuses] = useState<ClipStatusItem[]>([])
|
||||
const genTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
/* ── 播放 ── */
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
@@ -772,58 +740,6 @@ const EditingPlanner: React.FC = () => {
|
||||
setStickerSettings(config)
|
||||
}, [])
|
||||
|
||||
/** 构建剪辑计划 config(编辑器状态 → API config) */
|
||||
const buildPlanConfig = (): EditPlanConfig => ({
|
||||
title_config: titleConfig,
|
||||
subtitle_config: {
|
||||
enabled: subtitleSettings.enabled,
|
||||
position: subtitleSettings.position,
|
||||
font: subtitleSettings.font,
|
||||
color: subtitleSettings.fontColor,
|
||||
size: subtitleSettings.fontSize,
|
||||
animation: subtitleSettings.animation,
|
||||
},
|
||||
bgm_config: {
|
||||
enabled: bgmSettings.enabled,
|
||||
music_id: bgmSettings.music_id,
|
||||
},
|
||||
estimated_duration: totalDuration,
|
||||
segments: clips.map((c, i) => ({
|
||||
segment_order: i,
|
||||
duration_min: Math.max(1, c.duration - 2),
|
||||
duration_max: c.duration + 2,
|
||||
material_type: c.type === "voice" ? "voiceover" : "video",
|
||||
transition: c.transition
|
||||
? { type: c.transition.type, duration: c.transition.duration }
|
||||
: undefined,
|
||||
playback_speed: c.speed ? c.speed.rate : undefined,
|
||||
tts_config: c.tts_config
|
||||
? {
|
||||
mode: c.tts_config.mode,
|
||||
text: c.tts_config.text,
|
||||
voice_id: c.tts_config.voice_id,
|
||||
speed: c.tts_config.speed,
|
||||
pitch: c.tts_config.pitch,
|
||||
volume: c.tts_config.volume,
|
||||
subtitle_sync: c.tts_config.subtitle_sync,
|
||||
}
|
||||
: undefined,
|
||||
trim_config: c.trim_config
|
||||
? {
|
||||
start_time: c.trim_config.start_time,
|
||||
end_time: c.trim_config.end_time,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
watermark_config: { ...watermarkSettings },
|
||||
intro_outro_config: { ...introOutroSettings },
|
||||
pip_config: { ...pipSettings },
|
||||
filter_config: { ...filterSettings },
|
||||
green_screen_config: { ...chromaKeySettings },
|
||||
sticker_config: { ...stickerSettings },
|
||||
cover_config: { ...coverConfig },
|
||||
})
|
||||
|
||||
/* 保存 — 无论是否已加载模板,都打开保存弹窗;未加载时创建新模板 */
|
||||
const handleOpenSaveModal = () => {
|
||||
setSaveModalOpen(true)
|
||||
@@ -908,246 +824,6 @@ const EditingPlanner: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地编辑的片段同步到后端 clips 表
|
||||
* 策略:先删除后端所有片段,再批量创建(简单可靠,生成前使用)
|
||||
*/
|
||||
const syncClipsToBackend = async (planId: string): Promise<void> => {
|
||||
if (clips.length === 0) return
|
||||
|
||||
// 1. 获取并删除后端现有片段
|
||||
const existing = await getEditPlanClips(planId, { limit: 500 })
|
||||
if (existing.items.length > 0) {
|
||||
await batchDeleteEditPlanClips(
|
||||
planId,
|
||||
existing.items.map((c) => c.id),
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 批量创建新片段(并发 3 个)
|
||||
const clipDataList: CreateEditPlanClipRequest[] = clips.map((c, i) => ({
|
||||
clip_type: c.type === "voice" ? "voiceover" : "main",
|
||||
order: i,
|
||||
asset_id: c.media_asset_id || "",
|
||||
text_content: c.script_text || "",
|
||||
start_time: 0,
|
||||
duration: c.duration,
|
||||
transition_effect: c.transition?.type || "cut",
|
||||
transition_duration: c.transition?.duration || 0,
|
||||
playback_speed: c.speed?.rate || 1.0,
|
||||
config: {
|
||||
tts_config: c.tts_config || null,
|
||||
trim_config: c.trim_config || null,
|
||||
template_segment_id: c.template_segment_id || null,
|
||||
},
|
||||
}))
|
||||
|
||||
// 并发控制:最多同时 3 个请求
|
||||
const results: EditPlanClip[] = []
|
||||
const concurrency = 3
|
||||
for (let i = 0; i < clipDataList.length; i += concurrency) {
|
||||
const batch = clipDataList.slice(i, i + concurrency)
|
||||
const batchResults = await Promise.all(batch.map((data) => createEditPlanClip(planId, data)))
|
||||
results.push(...batchResults)
|
||||
}
|
||||
|
||||
console.log(`[片段同步] 创建了 ${results.length} 个片段`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 剪辑计划生成
|
||||
* 1. 有 planId → 更新计划配置 + 同步片段 + 触发生成
|
||||
* 2. 无 planId(从模板库直接进入)→ 先创建计划 + 同步片段 + 触发生成
|
||||
* 3. 触发生成后轮询状态,完成后获取视频结果
|
||||
*/
|
||||
const handleGoToGenerate = async () => {
|
||||
if (!loadedTemplateId) {
|
||||
message.warning("请先选择一个模板")
|
||||
return
|
||||
}
|
||||
if (clips.length === 0) {
|
||||
message.warning("请先添加片段")
|
||||
return
|
||||
}
|
||||
|
||||
setGenerating(true)
|
||||
setGenerated(false)
|
||||
setGeneratedVideos([])
|
||||
setGenError(null)
|
||||
setGenProgress(0)
|
||||
setGenCancelled(false)
|
||||
|
||||
try {
|
||||
const config = buildPlanConfig()
|
||||
let planId = loadedPlanId
|
||||
|
||||
if (planId) {
|
||||
// 已有计划 → 先重置状态为 draft(failed/editing 等非 draft 状态会被后端拒绝更新和生成)
|
||||
try {
|
||||
await updateEditPlan(planId, { status: "draft" })
|
||||
} catch (resetErr) {
|
||||
console.warn("[状态重置跳过]", resetErr)
|
||||
}
|
||||
// 再更新配置
|
||||
try {
|
||||
await updateEditPlan(planId, {
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
})
|
||||
} catch (updateErr) {
|
||||
console.warn("[计划更新跳过]", updateErr)
|
||||
}
|
||||
} else {
|
||||
// 无计划 → 创建新计划
|
||||
const plan = await createEditPlan({
|
||||
template_id: loadedTemplateId,
|
||||
name: draftName || "未命名计划",
|
||||
config,
|
||||
total_duration: totalDuration,
|
||||
})
|
||||
planId = plan.id
|
||||
setLoadedPlanId(planId)
|
||||
// 更新 URL 参数(不刷新页面)
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set("planId", planId)
|
||||
window.history.replaceState(null, "", `?${params.toString()}`)
|
||||
}
|
||||
|
||||
// 同步片段到后端 clips 表(生成前必须同步,后端生成从 clips 表读)
|
||||
try {
|
||||
await syncClipsToBackend(planId)
|
||||
} catch (syncErr) {
|
||||
console.warn("[片段同步失败]", syncErr)
|
||||
message.warning("片段同步失败,将使用模板默认配置生成")
|
||||
// 同步失败不阻塞生成,后端有模板兜底
|
||||
}
|
||||
|
||||
// 触发生成
|
||||
const genRes = await generateEditPlan(planId)
|
||||
setGenTotalClips(genRes.clip_count)
|
||||
message.info("已提交生成,等待处理...")
|
||||
|
||||
// 开始轮询
|
||||
startPolling(planId)
|
||||
} catch (err) {
|
||||
console.error("[生成失败]", err)
|
||||
setGenError("生成提交失败,请重试")
|
||||
setGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询生成状态,每 2 秒一次 */
|
||||
const startPolling = (planId: string) => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const status = await getGenerationStatus(planId)
|
||||
|
||||
// 计算进度
|
||||
const total = status.clips.length || genTotalClips
|
||||
const done = status.clips.filter(
|
||||
(c) => c.status === "completed" || c.status === "failed",
|
||||
).length
|
||||
setGenDoneClips(done)
|
||||
setGenTotalClips(total)
|
||||
setGenClipStatuses(status.clips || [])
|
||||
setGenProgress(total > 0 ? Math.round((done / total) * 100) : 5)
|
||||
|
||||
if (status.plan_status === "completed") {
|
||||
setGenProgress(100)
|
||||
setGenerating(false)
|
||||
setGenerated(true)
|
||||
|
||||
// 获取视频结果
|
||||
if (status.generation_task_id) {
|
||||
try {
|
||||
const videos = await getGenerationTaskResults(status.generation_task_id)
|
||||
setGeneratedVideos(videos)
|
||||
} catch (e) {
|
||||
console.error("[获取视频结果失败]", e)
|
||||
}
|
||||
}
|
||||
message.success("视频生成完成!")
|
||||
return // 停止轮询
|
||||
}
|
||||
|
||||
if (status.plan_status === "failed") {
|
||||
setGenerating(false)
|
||||
setGenError("生成失败,请重试")
|
||||
return // 停止轮询
|
||||
}
|
||||
|
||||
if (status.plan_status === "cancelled") {
|
||||
setGenerating(false)
|
||||
setGenError("生成已取消")
|
||||
setGenCancelled(true)
|
||||
message.info("生成任务已取消")
|
||||
return // 停止轮询
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
genTimerRef.current = setTimeout(poll, 2000)
|
||||
} catch (err) {
|
||||
console.error("[轮询状态失败]", err)
|
||||
genTimerRef.current = setTimeout(poll, 5000) // 出错后 5 秒重试
|
||||
}
|
||||
}
|
||||
|
||||
// 首次延迟 2 秒后开始
|
||||
genTimerRef.current = setTimeout(poll, 2000)
|
||||
}
|
||||
|
||||
/** 清理轮询定时器 */
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (genTimerRef.current) clearTimeout(genTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 取消生成任务 */
|
||||
const handleCancelGeneration = async () => {
|
||||
const targetId = loadedPlanId
|
||||
if (!targetId) return
|
||||
|
||||
Modal.confirm({
|
||||
title: "确认取消生成",
|
||||
content: "取消后已开始的生成任务,已生成的片段不会保留。确定要取消吗?",
|
||||
okText: "确认取消",
|
||||
cancelText: "继续生成",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
setCancelling(true)
|
||||
await cancelGeneration(targetId)
|
||||
message.success("已提交取消请求")
|
||||
// 轮询会继续运行直到检测到 cancelled 状态
|
||||
} catch (err) {
|
||||
console.error("[取消失败]", err)
|
||||
message.error("取消失败,请稍后重试")
|
||||
} finally {
|
||||
setCancelling(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/* 查看生成历史 */
|
||||
const handleViewGenHistory = async () => {
|
||||
const targetId = loadedPlanId || loadedTemplateId
|
||||
if (!targetId) {
|
||||
message.warning("请先加载一个模板或计划")
|
||||
return
|
||||
}
|
||||
setGenHistoryOpen(true)
|
||||
setGenHistoryLoading(true)
|
||||
try {
|
||||
const items = await getEditPlanGenerations(targetId)
|
||||
setGenHistory(items)
|
||||
} catch {
|
||||
message.error("加载生成历史失败")
|
||||
} finally {
|
||||
setGenHistoryLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────── 渲染 ──────────── */
|
||||
|
||||
@@ -1157,7 +833,7 @@ const EditingPlanner: React.FC = () => {
|
||||
<div className="ep-top-bar">
|
||||
<div className="ep-top-bar-left">
|
||||
<span className="ep-logo">✂️</span>
|
||||
<span className="ep-app-title">小虾剪辑编排器</span>
|
||||
<span className="ep-app-title">模板制作</span>
|
||||
<span className="ep-divider">|</span>
|
||||
<span className="ep-template-name">{currentTemplate?.name || "未选择模板"}</span>
|
||||
</div>
|
||||
@@ -1181,13 +857,6 @@ const EditingPlanner: React.FC = () => {
|
||||
<button className="ep-btn ep-btn-secondary" onClick={handleOpenSaveModal}>
|
||||
💾 保存模板
|
||||
</button>
|
||||
<button
|
||||
className="ep-btn ep-btn-primary"
|
||||
onClick={handleGoToGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
{loadedPlanId ? "🎬 生成视频" : "🎬 创建计划并生成"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1352,10 +1021,6 @@ const EditingPlanner: React.FC = () => {
|
||||
<span>🎬 {MODE_LABELS[currentMode]}</span>
|
||||
<span className="ep-status-sep">|</span>
|
||||
<span>📐 模板片段: {currentTemplate?.segments.length || 0}</span>
|
||||
<span className="ep-status-sep">|</span>
|
||||
<button className="ep-status-link" onClick={handleViewGenHistory}>
|
||||
📋 生成历史
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1376,214 +1041,6 @@ const EditingPlanner: React.FC = () => {
|
||||
onCancel={() => setSaveModalOpen(false)}
|
||||
/>
|
||||
|
||||
{/* ═══ 生成历史弹窗 ═══ */}
|
||||
<GenerationHistoryModal
|
||||
open={genHistoryOpen}
|
||||
loading={genHistoryLoading}
|
||||
history={genHistory}
|
||||
onClose={() => setGenHistoryOpen(false)}
|
||||
onCancel={async () => {
|
||||
Modal.confirm({
|
||||
title: "确认取消生成",
|
||||
content: "确定要取消这个生成任务吗?此操作不可恢复。",
|
||||
okText: "确认取消",
|
||||
cancelText: "再等等",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
if (!loadedPlanId) return
|
||||
try {
|
||||
await cancelGeneration(loadedPlanId)
|
||||
message.success("已提交取消请求")
|
||||
// 刷新历史列表
|
||||
handleViewGenHistory()
|
||||
} catch (err) {
|
||||
console.error("[取消失败]", err)
|
||||
message.error("取消失败,请稍后重试")
|
||||
}
|
||||
},
|
||||
})
|
||||
}}
|
||||
cancelLoading={cancelling}
|
||||
/>
|
||||
|
||||
{/* ═══ 生成进度弹窗 ═══ */}
|
||||
<Modal
|
||||
title={
|
||||
genError
|
||||
? "生成失败"
|
||||
: genCancelled
|
||||
? "已取消生成"
|
||||
: generated
|
||||
? "生成完成"
|
||||
: "正在生成视频"
|
||||
}
|
||||
open={generating || generated || !!genError || genCancelled}
|
||||
footer={
|
||||
generated
|
||||
? [
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setGenerated(false)
|
||||
setGenerating(false)
|
||||
setGenError(null)
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
generatedVideos.length > 0 && (
|
||||
<Button
|
||||
key="download"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
const v = generatedVideos[0]
|
||||
const url = v.download_url || v.file_url
|
||||
if (url) {
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = v.name || "video.mp4"
|
||||
a.target = "_blank"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
}}
|
||||
>
|
||||
下载视频
|
||||
</Button>
|
||||
),
|
||||
]
|
||||
: generating
|
||||
? [
|
||||
<Button key="cancel" danger loading={cancelling} onClick={handleCancelGeneration}>
|
||||
取消生成
|
||||
</Button>,
|
||||
]
|
||||
: genCancelled
|
||||
? [
|
||||
<Button
|
||||
key="close"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setGenCancelled(false)
|
||||
setGenerating(false)
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
]
|
||||
: genError
|
||||
? [
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setGenError(null)
|
||||
setGenerating(false)
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
]
|
||||
: null
|
||||
}
|
||||
closable={!generating}
|
||||
maskClosable={false}
|
||||
width={520}
|
||||
>
|
||||
{generating && (
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<Progress percent={genProgress} status="active" />
|
||||
<p style={{ marginTop: 8, color: "var(--text-secondary)" }}>
|
||||
已处理 {genDoneClips}/{genTotalClips} 个片段
|
||||
</p>
|
||||
{genClipStatuses.length > 0 && (
|
||||
<div className="ep-gen-clip-list">
|
||||
{genClipStatuses.map((clip, index) => (
|
||||
<div key={clip.clip_id || index} className="ep-gen-clip-item">
|
||||
<span className="ep-gen-clip-index">{index + 1}</span>
|
||||
<span className="ep-gen-clip-name">
|
||||
{clip.text_content
|
||||
? clip.text_content.slice(0, 20)
|
||||
: clip.clip_type || `片段${index + 1}`}
|
||||
</span>
|
||||
<span className={`ep-gen-clip-status status-${clip.status}`}>
|
||||
{clip.status === "completed"
|
||||
? "✓ 完成"
|
||||
: clip.status === "failed"
|
||||
? "✗ 失败"
|
||||
: clip.status === "processing"
|
||||
? "⟳ 处理中"
|
||||
: "⏳ 等待中"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
fontSize: 12,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
请耐心等待,生成过程中请勿关闭页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{genCancelled && (
|
||||
<div style={{ padding: "24px 0", textAlign: "center" }}>
|
||||
<p>生成已取消</p>
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: 12 }}>你可以继续编辑后重新生成</p>
|
||||
</div>
|
||||
)}
|
||||
{generated && generatedVideos.length > 0 && (
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<video
|
||||
src={generatedVideos[0].file_url || generatedVideos[0].download_url}
|
||||
controls
|
||||
preload="metadata"
|
||||
style={{ width: "100%", maxHeight: 320, borderRadius: 8 }}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
color: "var(--text-secondary)",
|
||||
}}
|
||||
>
|
||||
{generatedVideos[0].name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{generated && !generatedVideos.length && (
|
||||
<div style={{ padding: "24px 0", textAlign: "center" }}>
|
||||
<p>生成完成,但暂未获取到视频结果</p>
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: 12 }}>
|
||||
请稍后在剪辑计划列表中查看
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{genError && (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 0",
|
||||
textAlign: "center",
|
||||
color: "#ff4d4f",
|
||||
}}
|
||||
>
|
||||
<p>{genError}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setGenError(null)
|
||||
setGenerating(false)
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* ═══ BGM 选择器 Drawer ═══ */}
|
||||
<BgmSelector
|
||||
open={bgmDrawerOpen}
|
||||
|
||||
@@ -111,7 +111,8 @@ const MyTemplates: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleGenerate = (tpl: EditingTemplate) => {
|
||||
navigate(`/editing-planner?template=${tpl.id}&generate=1`)
|
||||
// 跳转到智能剪辑页面,统一从智能剪辑出片
|
||||
navigate(`/generate?templateId=${tpl.id}`)
|
||||
}
|
||||
|
||||
const handleCopy = (tpl: EditingTemplate) => {
|
||||
|
||||
Reference in New Issue
Block a user