Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1764d2c29f | |||
| b6e89b16a8 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ const ClipPropertiesPanel: React.FC<ClipPropertiesPanelProps> = ({
|
||||
onOpenFilterDrawer,
|
||||
onOpenGreenScreenDrawer,
|
||||
onOpenStickerDrawer,
|
||||
onOpenCoverDrawer,
|
||||
}) => {
|
||||
const { previewingId, handlePreviewVoice, stopPreview } = useVoicePreview()
|
||||
|
||||
@@ -140,6 +141,21 @@ const ClipPropertiesPanel: React.FC<ClipPropertiesPanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ═══ 封面设置 ═══ */}
|
||||
<div className="ep-settings-section">
|
||||
<div className="ep-section-title">
|
||||
<span className="ep-section-icon">🖼️</span>
|
||||
封面设置
|
||||
</div>
|
||||
{onOpenCoverDrawer && (
|
||||
<button className="ep-advanced-btn" onClick={onOpenCoverDrawer}>
|
||||
<span className="ep-advanced-btn-icon">🖼️</span>
|
||||
<span className="ep-advanced-btn-label">配置视频封面</span>
|
||||
<span className="ep-advanced-btn-arrow">›</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ═══ 片段详情(选中时显示) ═══ */}
|
||||
{selectedClip && (
|
||||
<ClipDetailSection
|
||||
|
||||
@@ -10,6 +10,7 @@ import PipConfigPanel from "./PipConfigPanel"
|
||||
import FilterPanel from "./FilterPanel"
|
||||
import GreenScreenPanel from "./GreenScreenPanel"
|
||||
import StickerPanel from "./StickerPanel"
|
||||
import CoverSelector from "./CoverSelector"
|
||||
import type {
|
||||
ClipData,
|
||||
TransitionConfig,
|
||||
@@ -23,6 +24,7 @@ import type {
|
||||
StickerConfig,
|
||||
} from "../types"
|
||||
import type { SubtitleStyleConfig } from "../types/subtitle"
|
||||
import type { CoverConfig } from "../types/cover"
|
||||
import type { BgmMixConfig } from "@/api/bgm"
|
||||
import { DEFAULT_TRANSITION, DEFAULT_SPEED, DEFAULT_TTS_CONFIG } from "../types"
|
||||
|
||||
@@ -85,6 +87,13 @@ interface EditorDrawersProps {
|
||||
stickerSettings: StickerConfig
|
||||
onStickerChange: (config: StickerConfig) => void
|
||||
onCloseStickerDrawer: () => void
|
||||
// 封面
|
||||
coverDrawerOpen: boolean
|
||||
onCloseCoverDrawer: () => void
|
||||
coverConfig: CoverConfig
|
||||
setCoverConfig: (config: CoverConfig | ((prev: CoverConfig) => CoverConfig)) => void
|
||||
templateId: string
|
||||
totalDuration: number
|
||||
}
|
||||
|
||||
const EditorDrawers: React.FC<EditorDrawersProps> = ({
|
||||
@@ -135,6 +144,11 @@ const EditorDrawers: React.FC<EditorDrawersProps> = ({
|
||||
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<EditorDrawersProps> = ({
|
||||
onChange={onStickerChange}
|
||||
totalDuration={totalDuration}
|
||||
/>
|
||||
|
||||
{/* 封面选择器 Drawer */}
|
||||
<CoverSelector
|
||||
open={coverDrawerOpen}
|
||||
onClose={onCloseCoverDrawer}
|
||||
config={coverConfig}
|
||||
onChange={setCoverConfig}
|
||||
totalDuration={totalDuration}
|
||||
templateId={templateId}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<RightPanelProps> = ({
|
||||
onOpenFilterDrawer,
|
||||
onOpenGreenScreenDrawer,
|
||||
onOpenStickerDrawer,
|
||||
onOpenCoverDrawer,
|
||||
clips,
|
||||
selectedClipId,
|
||||
onClipSelect,
|
||||
@@ -142,6 +144,7 @@ const RightPanel: React.FC<RightPanelProps> = ({
|
||||
onOpenFilterDrawer={onOpenFilterDrawer}
|
||||
onOpenGreenScreenDrawer={onOpenGreenScreenDrawer}
|
||||
onOpenStickerDrawer={onOpenStickerDrawer}
|
||||
onOpenCoverDrawer={onOpenCoverDrawer}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -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<CoverSelectorProps> = ({
|
||||
@@ -22,6 +24,7 @@ const CoverSelector: React.FC<CoverSelectorProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
totalDuration,
|
||||
templateId,
|
||||
}) => {
|
||||
const {
|
||||
fileInputRef,
|
||||
@@ -36,6 +39,31 @@ const CoverSelector: React.FC<CoverSelectorProps> = ({
|
||||
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 (
|
||||
<Drawer
|
||||
title="封面选择"
|
||||
@@ -133,12 +161,33 @@ const CoverSelector: React.FC<CoverSelectorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 生成封面按钮 */}
|
||||
{config.enabled && config.mode === "auto" && (
|
||||
<div className="cover-generate-section">
|
||||
<button
|
||||
className="cover-generate-btn"
|
||||
onClick={handleGenerateCover}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? "生成中..." : "🤖 AI 生成封面"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部 */}
|
||||
<div className="cover-footer">
|
||||
<button className="cover-reset-btn" onClick={handleReset}>
|
||||
重置封面
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 生成进度弹窗 */}
|
||||
<Modal open={generating} closable={false} footer={null} centered>
|
||||
<div style={{ textAlign: "center", padding: "24px 0" }}>
|
||||
<Spin size="large" />
|
||||
<p style={{ marginTop: 16, fontSize: 14, color: "#666" }}>AI 正在生成封面,请稍候...</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -68,4 +68,5 @@ export interface ClipPropertiesPanelProps {
|
||||
/** 打开贴纸面板 Drawer */
|
||||
onOpenStickerDrawer?: () => void
|
||||
/** 打开封面选择器 Drawer */
|
||||
onOpenCoverDrawer?: () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user