From eee0eee359138e85cc31fd600a4962e36702235e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 19:18:15 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(P1):=20#557=20=E5=89=AA=E8=BE=91?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E6=94=B9=E4=B8=BA=E7=BA=AF=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=BC=96=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除编辑器内所有直接生成视频入口(生成按钮、生成进度、生成历史) - 删除相关API调用:generateEditPlan/cancelGeneration/getGenerationStatus等 - 删除GenerationHistoryModal引用 - 编辑器标题改为「模板制作」,面包屑同步更新 - 我的模板页「使用模板生成」改为跳转智能剪辑页面 - 保留模板制作/保存/预览核心功能 - 保留标题/封面只读配置(模板数据的一部分,保存时一并存储) --- apps/web/src/components/layout/PageHead.tsx | 2 +- .../pages/editing-planner/EditingPlanner.tsx | 549 +----------------- .../src/pages/my-templates/MyTemplates.tsx | 3 +- 3 files changed, 6 insertions(+), 548 deletions(-) diff --git a/apps/web/src/components/layout/PageHead.tsx b/apps/web/src/components/layout/PageHead.tsx index 5e90cc661..46b71d2c1 100644 --- a/apps/web/src/components/layout/PageHead.tsx +++ b/apps/web/src/components/layout/PageHead.tsx @@ -59,7 +59,7 @@ const ROUTE_TITLE_MAP: Record = { "/app/subscription/upgrade": "升级订阅", "/app/subscription/billing": "账单管理", "/app/profile": "个人设置", - "/app/editing-planner": "剪辑规划", + "/app/editing-planner": "模板制作", "/app/my-templates": "我的模板", "/app/voice-clone": "我的音色", "/app/voice-materials": "配音库", diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 810e0ed51..dc5e306c3 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -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([]) - const [genHistoryLoading, setGenHistoryLoading] = useState(false) - /* ── 剪辑计划(从列表页编辑进入时) ── */ const [loadedPlanId, setLoadedPlanId] = useState(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([]) - const [genError, setGenError] = useState(null) - const [cancelling, setCancelling] = useState(false) - const [genCancelled, setGenCancelled] = useState(false) - const [genClipStatuses, setGenClipStatuses] = useState([]) - const genTimerRef = useRef | 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 => { - 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 = () => {
✂️ - 小虾剪辑编排器 + 模板制作 | {currentTemplate?.name || "未选择模板"}
@@ -1181,13 +857,6 @@ const EditingPlanner: React.FC = () => { -
@@ -1352,10 +1021,6 @@ const EditingPlanner: React.FC = () => { 🎬 {MODE_LABELS[currentMode]} | 📐 模板片段: {currentTemplate?.segments.length || 0} - | - @@ -1376,214 +1041,6 @@ const EditingPlanner: React.FC = () => { onCancel={() => setSaveModalOpen(false)} /> - {/* ═══ 生成历史弹窗 ═══ */} - 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} - /> - - {/* ═══ 生成进度弹窗 ═══ */} - { - setGenerated(false) - setGenerating(false) - setGenError(null) - }} - > - 关闭 - , - generatedVideos.length > 0 && ( - - ), - ] - : generating - ? [ - , - ] - : genCancelled - ? [ - , - ] - : genError - ? [ - , - ] - : null - } - closable={!generating} - maskClosable={false} - width={520} - > - {generating && ( -
- -

- 已处理 {genDoneClips}/{genTotalClips} 个片段 -

- {genClipStatuses.length > 0 && ( -
- {genClipStatuses.map((clip, index) => ( -
- {index + 1} - - {clip.text_content - ? clip.text_content.slice(0, 20) - : clip.clip_type || `片段${index + 1}`} - - - {clip.status === "completed" - ? "✓ 完成" - : clip.status === "failed" - ? "✗ 失败" - : clip.status === "processing" - ? "⟳ 处理中" - : "⏳ 等待中"} - -
- ))} -
- )} -

- 请耐心等待,生成过程中请勿关闭页面 -

-
- )} - {genCancelled && ( -
-

生成已取消

-

你可以继续编辑后重新生成

-
- )} - {generated && generatedVideos.length > 0 && ( -
-
- )} - {generated && !generatedVideos.length && ( -
-

生成完成,但暂未获取到视频结果

-

- 请稍后在剪辑计划列表中查看 -

-
- )} - {genError && ( -
-

{genError}

- -
- )} -
- {/* ═══ BGM 选择器 Drawer ═══ */} { } const handleGenerate = (tpl: EditingTemplate) => { - navigate(`/editing-planner?template=${tpl.id}&generate=1`) + // 跳转到智能剪辑页面,统一从智能剪辑出片 + navigate(`/generate?templateId=${tpl.id}`) } const handleCopy = (tpl: EditingTemplate) => { -- 2.54.0 From eba78978db98ef8bce24cad8264ee58d214f4643 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 19:25:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(lint):=20=E5=88=A0=E9=99=A4=E6=9C=AA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84import=E5=92=8Cstate=E5=8F=98?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/editing-planner/EditingPlanner.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index dc5e306c3..aea3b768a 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -29,8 +29,6 @@ import { getMediaAssets, getEditPlan, getEditPlanClips, - type EditPlanClip, - type CreateEditPlanClipRequest, } from "@/api/editPlans" import { useUndoRedo } from "./hooks/useUndoRedo" import type { @@ -217,7 +215,7 @@ const EditingPlanner: React.FC = () => { } /* ── 剪辑计划(从列表页编辑进入时) ── */ - const [loadedPlanId, setLoadedPlanId] = useState(urlPlanId || null) + const [loadedPlanId] = useState(urlPlanId || null) /* ── 播放 ── */ const [isPlaying, setIsPlaying] = useState(false) -- 2.54.0 From f0c0ab6dfe13a62e213c0f9140b91b17b6067e95 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 19:29:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?style:=20prettier=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/editing-planner/EditingPlanner.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index aea3b768a..503fbfc71 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -20,16 +20,8 @@ import { getTemplateCategories, MODE_LABELS, } from "@/api/editingPlanner" -import type { - MediaAsset, - TransitionEffect, - TitleConfig, -} from "@/api/editPlans" -import { - getMediaAssets, - getEditPlan, - getEditPlanClips, -} from "@/api/editPlans" +import type { MediaAsset, TransitionEffect, TitleConfig } from "@/api/editPlans" +import { getMediaAssets, getEditPlan, getEditPlanClips } from "@/api/editPlans" import { useUndoRedo } from "./hooks/useUndoRedo" import type { ClipData, @@ -822,7 +814,6 @@ const EditingPlanner: React.FC = () => { } } - /* ──────────── 渲染 ──────────── */ return ( -- 2.54.0