diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.css b/apps/web/src/pages/editing-planner/EditingPlanner.css index d62b5e7dd..36a06cd3f 100644 --- a/apps/web/src/pages/editing-planner/EditingPlanner.css +++ b/apps/web/src/pages/editing-planner/EditingPlanner.css @@ -6408,3 +6408,35 @@ padding: 16px; text-align: center; } + +/* ═══ 封面 AI 生成按钮 ═══ */ +.cover-generate-section { + padding: 0 16px 12px; +} + +.cover-generate-btn { + width: 100%; + padding: 10px 16px; + border: 1px solid var(--color-primary, #1677ff); + border-radius: 8px; + background: var(--color-primary, #1677ff); + color: #fff; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.cover-generate-btn:hover:not(:disabled) { + background: var(--color-primary-hover, #4096ff); + border-color: var(--color-primary-hover, #4096ff); +} + +.cover-generate-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 5e0f08a86..9411865e3 100644 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -243,6 +243,7 @@ const EditingPlanner: React.FC = () => { onOpenFilterDrawer={() => drawers.setFilterDrawerOpen(true)} onOpenGreenScreenDrawer={() => drawers.setChromaKeyDrawerOpen(true)} onOpenStickerDrawer={() => drawers.setStickerDrawerOpen(true)} + onOpenCoverDrawer={() => drawers.setCoverDrawerOpen(true)} clips={clips} selectedClipId={clipOps.selectedClipId} onClipSelect={clipOps.handleClipSelect} @@ -335,6 +336,12 @@ const EditingPlanner: React.FC = () => { stickerSettings={stickerSettings} onStickerChange={setStickerSettings} onCloseStickerDrawer={() => drawers.setStickerDrawerOpen(false)} + coverDrawerOpen={drawers.coverDrawerOpen} + onCloseCoverDrawer={() => drawers.setCoverDrawerOpen(false)} + coverConfig={coverConfig} + setCoverConfig={setCoverConfig} + templateId={urlTemplateId} + totalDuration={totalDuration} /> ) diff --git a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx index ce9674d0f..e635332d6 100644 --- a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx +++ b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx @@ -35,6 +35,7 @@ const ClipPropertiesPanel: React.FC = ({ onOpenFilterDrawer, onOpenGreenScreenDrawer, onOpenStickerDrawer, + onOpenCoverDrawer, }) => { const { previewingId, handlePreviewVoice, stopPreview } = useVoicePreview() @@ -140,6 +141,21 @@ const ClipPropertiesPanel: React.FC = ({ )} + {/* ═══ 封面设置 ═══ */} +
+
+ 🖼️ + 封面设置 +
+ {onOpenCoverDrawer && ( + + )} +
+ {/* ═══ 片段详情(选中时显示) ═══ */} {selectedClip && ( void onCloseStickerDrawer: () => void + // 封面 + coverDrawerOpen: boolean + onCloseCoverDrawer: () => void + coverConfig: CoverConfig + setCoverConfig: (config: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void + templateId: string + totalDuration: number } const EditorDrawers: React.FC = ({ @@ -135,6 +144,11 @@ const EditorDrawers: React.FC = ({ stickerSettings, onStickerChange, onCloseStickerDrawer, + coverDrawerOpen, + onCloseCoverDrawer, + coverConfig, + setCoverConfig, + templateId, }) => { const transitionConfig = transitionTargetClipId ? (clips.find((c) => c.id === transitionTargetClipId)?.transition ?? DEFAULT_TRANSITION) @@ -239,6 +253,16 @@ const EditorDrawers: React.FC = ({ onChange={onStickerChange} totalDuration={totalDuration} /> + + {/* 封面选择器 Drawer */} + ) } diff --git a/apps/web/src/pages/editing-planner/components/RightPanel.tsx b/apps/web/src/pages/editing-planner/components/RightPanel.tsx index 8d710ba50..f9e4601da 100644 --- a/apps/web/src/pages/editing-planner/components/RightPanel.tsx +++ b/apps/web/src/pages/editing-planner/components/RightPanel.tsx @@ -35,6 +35,7 @@ interface RightPanelProps { onOpenFilterDrawer: () => void onOpenGreenScreenDrawer: () => void onOpenStickerDrawer: () => void + onOpenCoverDrawer: () => void // 片段 tab clips: ClipData[] selectedClipId: string | null @@ -72,6 +73,7 @@ const RightPanel: React.FC = ({ onOpenFilterDrawer, onOpenGreenScreenDrawer, onOpenStickerDrawer, + onOpenCoverDrawer, clips, selectedClipId, onClipSelect, @@ -142,6 +144,7 @@ const RightPanel: React.FC = ({ onOpenFilterDrawer={onOpenFilterDrawer} onOpenGreenScreenDrawer={onOpenGreenScreenDrawer} onOpenStickerDrawer={onOpenStickerDrawer} + onOpenCoverDrawer={onOpenCoverDrawer} /> ) })()} diff --git a/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx b/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx index 6bb5a6d96..5ee92f1b9 100644 --- a/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx +++ b/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx @@ -2,8 +2,9 @@ * 封面选择器 * 抽帧选封面 + 上传自定义封面 + 智能封面推荐 */ -import React from "react" -import { Drawer } from "antd" +import React, { useState } from "react" +import { Drawer, Modal, Spin, message } from "antd" +import { generateCover } from "@/api/template-editor/aiFeatures" import type { CoverConfig, CoverMode } from "../../types" import { useCoverSelector, MODE_LABELS, MODE_ICONS } from "./useCoverSelector" import { CoverAutoMode, CoverFrameMode, CoverUploadMode } from "./CoverModePanels" @@ -14,6 +15,7 @@ interface CoverSelectorProps { config: CoverConfig onChange: (config: CoverConfig) => void totalDuration: number + templateId: string } const CoverSelector: React.FC = ({ @@ -22,6 +24,7 @@ const CoverSelector: React.FC = ({ config, onChange, totalDuration, + templateId, }) => { const { fileInputRef, @@ -36,6 +39,31 @@ const CoverSelector: React.FC = ({ formatTime, } = useCoverSelector({ config, onChange }) + const [generating, setGenerating] = useState(false) + + const handleGenerateCover = async () => { + if (!templateId) { + message.error("请先保存模板") + return + } + setGenerating(true) + try { + const res = await generateCover(templateId, { + asset_ids: [], + cover_type: "ai_frame", + }) + const imageUrl = res.cover?.image_url || res.cover?.thumbnail_url || "" + if (imageUrl) { + update({ upload_url: imageUrl, thumbnail_url: imageUrl }) + } + message.success("封面生成成功") + } catch (err: any) { + message.error("封面生成失败: " + (err?.message || "未知错误")) + } finally { + setGenerating(false) + } + } + return ( = ({ + {/* AI 生成封面按钮 */} + {config.enabled && config.mode === "auto" && ( +
+ +
+ )} + {/* 底部 */}
+ + {/* 生成进度弹窗 */} + +
+ +

AI 正在生成封面,请稍候...

+
+
) } diff --git a/apps/web/src/pages/editing-planner/hooks/useEditorDrawers.ts b/apps/web/src/pages/editing-planner/hooks/useEditorDrawers.ts index 310ae7010..ff44236c8 100644 --- a/apps/web/src/pages/editing-planner/hooks/useEditorDrawers.ts +++ b/apps/web/src/pages/editing-planner/hooks/useEditorDrawers.ts @@ -17,6 +17,7 @@ export const useEditorDrawers = () => { const [filterDrawerOpen, setFilterDrawerOpen] = useState(false) const [chromaKeyDrawerOpen, setChromaKeyDrawerOpen] = useState(false) const [stickerDrawerOpen, setStickerDrawerOpen] = useState(false) + const [coverDrawerOpen, setCoverDrawerOpen] = useState(false) /* ── 目标片段 ID ── */ /** 当前正在编辑转场的片段 ID(null = 全局默认转场) */ @@ -66,6 +67,8 @@ export const useEditorDrawers = () => { setChromaKeyDrawerOpen, stickerDrawerOpen, setStickerDrawerOpen, + coverDrawerOpen, + setCoverDrawerOpen, // 目标 ID transitionTargetClipId, speedTargetClipId, diff --git a/apps/web/src/pages/editing-planner/types/clipProperties.ts b/apps/web/src/pages/editing-planner/types/clipProperties.ts index 3f37015f2..51bdd29cf 100644 --- a/apps/web/src/pages/editing-planner/types/clipProperties.ts +++ b/apps/web/src/pages/editing-planner/types/clipProperties.ts @@ -68,4 +68,5 @@ export interface ClipPropertiesPanelProps { /** 打开贴纸面板 Drawer */ onOpenStickerDrawer?: () => void /** 打开封面选择器 Drawer */ + onOpenCoverDrawer?: () => void }