diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.css b/apps/web/src/pages/editing-planner/EditingPlanner.css old mode 100644 new mode 100755 index 6ac2b6a2a..f365b52df --- a/apps/web/src/pages/editing-planner/EditingPlanner.css +++ b/apps/web/src/pages/editing-planner/EditingPlanner.css @@ -6123,3 +6123,175 @@ 50% { opacity: 0.5; } } +/* ═══ 右侧栏 Tab ═══ */ +.ep-right-panel { + width: 260px; + height: 100%; + display: flex; + flex-direction: column; + border-left: 1px solid var(--border-color, #e5e7eb); + background: var(--bg-secondary, #f9fafb); +} + +.ep-right-tabs { + display: flex; + height: 40px; + border-bottom: 1px solid var(--border-color, #e5e7eb); + background: var(--bg-primary, #fff); +} + +.ep-right-tab { + flex: 1; + border: none; + background: transparent; + font-size: 13px; + color: var(--text-secondary, #6b7280); + cursor: pointer; + transition: all 0.2s; + border-bottom: 2px solid transparent; +} + +.ep-right-tab:hover { + color: var(--text-primary, #111827); +} + +.ep-right-tab.active { + color: var(--primary-color, #3b82f6); + border-bottom-color: var(--primary-color, #3b82f6); + font-weight: 500; +} + +.ep-right-tab-content { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +/* ═══ 编辑器内片段列表 ═══ */ +.ep-clip-list { + height: 100%; + display: flex; + flex-direction: column; +} + +.ep-clip-list-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color, #e5e7eb); + background: var(--bg-primary, #fff); +} + +.ep-clip-list-count { + font-size: 12px; + color: var(--text-secondary, #6b7280); +} + +.ep-clip-list-count b { + color: var(--text-primary, #111827); + font-weight: 600; +} + +.ep-clip-list-scroll { + flex: 1; + overflow-y: auto; + padding: 6px; +} + +.ep-clip-list-item { + background: var(--bg-primary, #fff); + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 6px; + padding: 8px 10px; + margin-bottom: 6px; + cursor: pointer; + transition: all 0.15s; +} + +.ep-clip-list-item:hover { + border-color: var(--primary-color, #3b82f6); + box-shadow: 0 1px 3px rgba(59, 130, 246, 0.1); +} + +.ep-clip-list-item.selected { + border-color: var(--primary-color, #3b82f6); + background: rgba(59, 130, 246, 0.04); +} + +.ep-clip-item-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.ep-clip-item-index { + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + font-size: 11px; + font-weight: 600; + background: var(--bg-tertiary, #f3f4f6); + color: var(--text-secondary, #6b7280); + border-radius: 3px; + flex-shrink: 0; +} + +.ep-clip-item-type { + font-size: 11px; + color: var(--text-secondary, #6b7280); + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.ep-clip-item-type-label { + font-size: 11px; +} + +.ep-clip-item-duration { + margin-left: auto; + font-size: 11px; + font-weight: 500; + color: var(--text-primary, #111827); + flex-shrink: 0; +} + +.ep-clip-item-text { + font-size: 12px; + color: var(--text-secondary, #6b7280); + line-height: 1.4; + margin-bottom: 6px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.ep-clip-item-actions { + display: flex; + gap: 2px; + justify-content: flex-end; + opacity: 0; + transition: opacity 0.15s; +} + +.ep-clip-list-item:hover .ep-clip-item-actions, +.ep-clip-list-item.selected .ep-clip-item-actions { + opacity: 1; +} + +.ep-clip-item-btn { + width: 24px !important; + height: 24px !important; + padding: 0 !important; + font-size: 11px !important; +} + +.ep-clip-list-empty { + padding: 16px; + text-align: center; +} diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index d0647d730..b8a6e1c44 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -85,6 +85,7 @@ import MediaPanel from "./components/MediaPanel"; import PreviewPlayer from "./components/PreviewPlayer"; import TimelinePanel from "./components/TimelinePanel"; import ClipPropertiesPanel from "./components/ClipPropertiesPanel"; +import EditorClipList from "./components/EditorClipList"; import BgmSelector from "./components/BgmSelector"; import SubtitleStylePanel from "./components/SubtitleStylePanel"; import type { SubtitleStyleConfig } from "./components/SubtitleStylePanel"; @@ -242,6 +243,8 @@ const EditingPlanner: React.FC = () => { ...DEFAULT_COVER_CONFIG, }); const [coverDrawerOpen, setCoverDrawerOpen] = useState(false); + /* ── 右侧栏 Tab ── */ + const [rightTab, setRightTab] = useState<"properties" | "clips">("properties"); /* ── 保存弹窗 ── */ const [saveModalOpen, setSaveModalOpen] = useState(false); @@ -465,6 +468,20 @@ const EditingPlanner: React.FC = () => { music_id: cfg.bgm_config!.music_id || "", })); } + // 还原封面配置 + if (cfg.cover_config) { + setCoverSettings((prev) => ({ + ...prev, + enabled: cfg.cover_config!.enabled ?? prev.enabled, + mode: (cfg.cover_config!.mode as CoverConfig["mode"]) || prev.mode, + frame_time: cfg.cover_config!.frame_time ?? prev.frame_time, + upload_url: cfg.cover_config!.upload_url || prev.upload_url, + thumbnail_url: + cfg.cover_config!.thumbnail_url || prev.thumbnail_url, + ai_suggested_time: + cfg.cover_config!.ai_suggested_time ?? prev.ai_suggested_time, + })); + } // 还原片段:优先从后端 clips 表,其次从 config.segments 兜底 const backendClips = clipsRes?.items || []; @@ -1349,43 +1366,87 @@ const EditingPlanner: React.FC = () => { {/* 右栏 260px:设置面板 */} - - setTitleSettings((prev) => ({ ...prev, ...partial })) - } - onSubtitleSettingsChange={(partial) => - setSubtitleSettings( - (prev) => ({ ...prev, ...partial }) as SubtitleStyleConfig, - ) - } - onBgmSettingsChange={(partial) => - setBgmSettings((prev) => ({ ...prev, ...partial })) - } - onClipUpdate={handleClipUpdate} - onOpenBgmDrawer={() => setBgmDrawerOpen(true)} - onOpenSubtitleDrawer={() => setSubtitleDrawerOpen(true)} - voiceMaterials={voiceMaterials} - voiceMaterialsLoading={voiceMaterialsQuery.isLoading} - onRefreshVoiceMaterials={() => voiceMaterialsQuery.refetch()} - onClipVoiceSelect={handleClipVoiceSelect} - onOpenTransitionDrawer={handleOpenTransitionDrawer} - onOpenSpeedDrawer={handleOpenSpeedDrawer} - onOpenTtsDrawer={handleOpenTtsDrawer} - onOpenWatermarkDrawer={() => setWatermarkDrawerOpen(true)} - onOpenIntroOutroDrawer={() => setIntroOutroDrawerOpen(true)} - onOpenPipDrawer={() => setPipDrawerOpen(true)} - onOpenFilterDrawer={() => setFilterDrawerOpen(true)} - onOpenGreenScreenDrawer={() => setChromaKeyDrawerOpen(true)} - onOpenStickerDrawer={() => setStickerDrawerOpen(true)} - onOpenCoverDrawer={() => setCoverDrawerOpen(true)} - /> +
+ {/* Tab 切换 */} +
+ + +
+ + {/* 属性 Tab */} + {rightTab === "properties" && ( +
+ + setTitleSettings((prev) => ({ ...prev, ...partial })) + } + onSubtitleSettingsChange={(partial) => + setSubtitleSettings( + (prev) => ({ ...prev, ...partial }) as SubtitleStyleConfig, + ) + } + onBgmSettingsChange={(partial) => + setBgmSettings((prev) => ({ ...prev, ...partial })) + } + onClipUpdate={handleClipUpdate} + onOpenBgmDrawer={() => setBgmDrawerOpen(true)} + onOpenSubtitleDrawer={() => setSubtitleDrawerOpen(true)} + voiceMaterials={voiceMaterials} + voiceMaterialsLoading={voiceMaterialsQuery.isLoading} + onRefreshVoiceMaterials={() => voiceMaterialsQuery.refetch()} + onClipVoiceSelect={handleClipVoiceSelect} + onOpenTransitionDrawer={handleOpenTransitionDrawer} + onOpenSpeedDrawer={handleOpenSpeedDrawer} + onOpenTtsDrawer={handleOpenTtsDrawer} + onOpenWatermarkDrawer={() => setWatermarkDrawerOpen(true)} + onOpenIntroOutroDrawer={() => setIntroOutroDrawerOpen(true)} + onOpenPipDrawer={() => setPipDrawerOpen(true)} + onOpenFilterDrawer={() => setFilterDrawerOpen(true)} + onOpenGreenScreenDrawer={() => setChromaKeyDrawerOpen(true)} + onOpenStickerDrawer={() => setStickerDrawerOpen(true)} + onOpenCoverDrawer={() => setCoverDrawerOpen(true)} + /> +
+ )} + + {/* 片段 Tab */} + {rightTab === "clips" && ( +
+ { + const idx = clips.findIndex((c) => c.id === clipId); + if (idx > 0) handleClipReorder(clipId, idx - 1); + }} + onMoveDown={(clipId) => { + const idx = clips.findIndex((c) => c.id === clipId); + if (idx < clips.length - 1) handleClipReorder(clipId, idx + 1); + }} + onRemove={handleClipRemove} + onAdd={handleAddClip} + /> +
+ )} +
{/* ═══ 第4行:底栏 40px ═══ */} diff --git a/apps/web/src/pages/editing-planner/components/EditorClipList.tsx b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx new file mode 100755 index 000000000..ae9b5c0cb --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/EditorClipList.tsx @@ -0,0 +1,166 @@ +/** + * 编辑器右侧栏 — 片段列表 Tab + * 紧凑版片段管理:选中、上下移动、删除、添加 + */ +import React from "react"; +import { Button, Tooltip, Empty } from "antd"; +import { + UpOutlined, + DownOutlined, + DeleteOutlined, + PlusOutlined, + ScissorOutlined, + SoundOutlined, + PictureOutlined, + VideoCameraOutlined, +} from "@ant-design/icons"; +import type { ClipData, ClipType } from "../types"; + +interface EditorClipListProps { + clips: ClipData[]; + selectedClipId: string | null; + onSelect: (clipId: string) => void; + onMoveUp: (clipId: string) => void; + onMoveDown: (clipId: string) => void; + onRemove: (clipId: string) => void; + onAdd: () => void; + onSplit?: (clipId: string) => void; +} + +const clipTypeIcon: Record = { + video: , + image: , + voice: , + pip: , +}; + +const clipTypeLabel: Record = { + video: "视频", + image: "图片", + voice: "配音", + pip: "画中画", +}; + +const formatDuration = (sec: number) => { + if (sec < 60) return `${sec.toFixed(1)}s`; + const m = Math.floor(sec / 60); + const s = (sec % 60).toFixed(0); + return `${m}m${s.padStart(2, "0")}s`; +}; + +const EditorClipList: React.FC = ({ + clips, + selectedClipId, + onSelect, + onMoveUp, + onMoveDown, + onRemove, + onAdd, +}) => { + if (clips.length === 0) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* 顶部工具栏 */} +
+ + 共 {clips.length} 个片段 + + + + +
+ + {/* 片段列表 */} +
+ {clips.map((clip, index) => ( +
onSelect(clip.id)} + > + {/* 序号 + 类型图标 */} +
+ {index + 1} + + {clipTypeIcon[clip.type] || } + + {clipTypeLabel[clip.type] || "片段"} + + + + {formatDuration(clip.duration)} + +
+ + {/* 文案预览 */} + {clip.script_text && ( +
+ {clip.script_text.slice(0, 40)} + {clip.script_text.length > 40 ? "..." : ""} +
+ )} + + {/* 操作按钮 */} +
e.stopPropagation()}> + +
+
+ ))} +
+
+ ); +}; + +export default EditorClipList;