185c240557
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m24s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m50s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Failing after 12m32s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 17m14s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 18m15s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 21m5s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 21m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m55s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m58s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m0s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 472h35m3s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 472h35m7s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 472h35m38s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 472h38m15s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 472h38m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 472h48m43s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 472h48m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 472h51m16s
CI/CD Pipeline / PR Build Web Image (push) Failing after 472h48m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 472h55m29s
CI/CD Pipeline / PR Build API Image (push) Failing after 472h48m49s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 472h55m31s
CI/CD Pipeline / Frontend Lint (push) Failing after 472h49m54s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 472h55m32s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 473h9m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 473h29m30s
模板编辑器只定义模板规则(片段数量、时长范围),不再承载素材管理:
- MediaPanel 移除'📁 素材'Tab 及 AssetSelector 引用
- EditingPlanner 移除 mediaAssets/selectedAssetIds state
- useTemplateManagement/useTemplateList 不再加载 getMediaAssets
- 不影响用户端智能剪辑流程的素材选择功能
325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
/**
|
|
* 模板编辑器 — 制作/编辑剪辑模板
|
|
* 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px)
|
|
*
|
|
* 主组件仅保留 Hook 组装与整体布局
|
|
* 全局配置 → hooks/useGlobalSettings
|
|
* 配音素材 → hooks/useVoiceMaterials
|
|
* 撤销重做 → hooks/useUndoRedo
|
|
* 抽屉管理 → hooks/useEditorDrawers
|
|
* 播放控制 → hooks/usePlaybackControl
|
|
* 片段操作 → hooks/useClipOperations
|
|
* 模板管理 → hooks/useTemplateManagement
|
|
*/
|
|
import React, { useState } from "react"
|
|
import { useSearchParams } from "react-router-dom"
|
|
import { MODE_LABELS } from "@/api/editing-planner"
|
|
import { MODE_LIST } from "./constants"
|
|
import MediaPanel from "./components/MediaPanel"
|
|
import PreviewPlayer from "./components/PreviewPlayer"
|
|
import TimelinePanel from "./components/TimelinePanel"
|
|
import TopBar from "./components/TopBar"
|
|
import ModeBar from "./components/ModeBar"
|
|
import RightPanel from "./components/RightPanel"
|
|
import StatusBar from "./components/StatusBar"
|
|
import EditorDrawers from "./components/EditorDrawers"
|
|
import SaveModal from "./components/SaveModal"
|
|
|
|
import { useUndoRedo } from "./hooks/useUndoRedo"
|
|
import { useEditorDrawers } from "./hooks/useEditorDrawers"
|
|
import { usePlaybackControl } from "./hooks/usePlaybackControl"
|
|
import { useClipOperations } from "./hooks/useClipOperations"
|
|
import { useTemplateManagement, FILTER_CATEGORIES } from "./hooks/useTemplateManagement"
|
|
import { useGlobalSettings } from "./hooks/useGlobalSettings"
|
|
import { useVoiceMaterials } from "./hooks/useVoiceMaterials"
|
|
|
|
import type { ClipData } from "./types"
|
|
import "./EditingPlanner.css"
|
|
|
|
const EditingPlanner: React.FC = () => {
|
|
const [searchParams] = useSearchParams()
|
|
const urlTemplateId = searchParams.get("templateId") || ""
|
|
const urlPlanId = searchParams.get("planId") || ""
|
|
|
|
/* ── 片段(撤销/重做) ── */
|
|
const {
|
|
state: clips,
|
|
set: setClips,
|
|
undo,
|
|
redo,
|
|
canUndo,
|
|
canRedo,
|
|
reset: resetClips,
|
|
} = useUndoRedo<ClipData[]>([])
|
|
|
|
/* ── 全局配置 ── */
|
|
const {
|
|
titleConfig,
|
|
setTitleConfig,
|
|
subtitleSettings,
|
|
setSubtitleSettings,
|
|
bgmSettings,
|
|
setBgmSettings,
|
|
watermarkSettings,
|
|
setWatermarkSettings,
|
|
introOutroSettings,
|
|
setIntroOutroSettings,
|
|
pipSettings,
|
|
setPipSettings,
|
|
filterSettings,
|
|
setFilterSettings,
|
|
chromaKeySettings,
|
|
setChromaKeySettings,
|
|
stickerSettings,
|
|
setStickerSettings,
|
|
} = useGlobalSettings()
|
|
|
|
/* ── 右侧栏 Tab ── */
|
|
const [rightTab, setRightTab] = useState<"properties" | "clips">("properties")
|
|
|
|
/* ── 配音素材 ── */
|
|
const {
|
|
voiceMaterials,
|
|
loading: voiceMaterialsLoading,
|
|
refetch: refetchVoiceMaterials,
|
|
} = useVoiceMaterials()
|
|
|
|
/* ── 派生计算 ── */
|
|
const totalDuration = clips.reduce((sum, c) => sum + c.duration, 0)
|
|
|
|
/* ── Hook: 抽屉管理 ── */
|
|
const drawers = useEditorDrawers()
|
|
|
|
/* ── Hook: 播放控制 ── */
|
|
const playback = usePlaybackControl(totalDuration)
|
|
|
|
/* ── Hook: 片段操作 ── */
|
|
const clipOps = useClipOperations({ clips, setClips })
|
|
|
|
/* ── Hook: 模板管理 ── */
|
|
const tpl = useTemplateManagement({
|
|
urlTemplateId,
|
|
urlPlanId,
|
|
resetClips,
|
|
setClips,
|
|
setSelectedClipId: clipOps.setSelectedClipId,
|
|
setTitleConfig,
|
|
setSubtitleSettings,
|
|
setBgmSettings,
|
|
clips,
|
|
totalDuration,
|
|
titleConfig,
|
|
subtitleSettings,
|
|
bgmSettings,
|
|
watermarkSettings,
|
|
introOutroSettings,
|
|
pipSettings,
|
|
filterSettings,
|
|
chromaKeySettings,
|
|
stickerSettings,
|
|
})
|
|
|
|
/* ──────────── 渲染 ──────────── */
|
|
|
|
return (
|
|
<div className="ep-v8-root">
|
|
{/* ═══ 第1行:顶栏 42px ═══ */}
|
|
<TopBar
|
|
currentTemplate={tpl.currentTemplate}
|
|
canUndo={canUndo}
|
|
canRedo={canRedo}
|
|
onUndo={undo}
|
|
onRedo={redo}
|
|
onOpenSaveModal={tpl.handleOpenSaveModal}
|
|
/>
|
|
|
|
{/* ═══ 第2行:模式栏 56px ═══ */}
|
|
<ModeBar
|
|
modeList={MODE_LIST}
|
|
currentMode={tpl.currentMode}
|
|
onModeChange={tpl.handleModeChange}
|
|
/>
|
|
|
|
{/* ═══ 第3行:三栏主体 ═══ */}
|
|
<div className="ep-main-body">
|
|
{/* 左栏 220px:模板列表 */}
|
|
<MediaPanel
|
|
templates={tpl.filteredTemplates}
|
|
loading={tpl.loadingTemplates}
|
|
searchQuery={tpl.searchQuery}
|
|
currentFilter={tpl.currentFilter}
|
|
filterCategories={FILTER_CATEGORIES}
|
|
loadedTemplateId={tpl.loadedTemplateId}
|
|
onLoadTemplate={tpl.handleLoadTemplate}
|
|
onSearchChange={tpl.setSearchQuery}
|
|
onFilterChange={tpl.setCurrentFilter}
|
|
/>
|
|
|
|
{/* 中栏 flex-1 */}
|
|
<div className="ep-center-col">
|
|
{/* 上半部:视频预览 + 封面预览 */}
|
|
<PreviewPlayer
|
|
clips={clips}
|
|
selectedClipId={clipOps.selectedClipId}
|
|
isPlaying={playback.isPlaying}
|
|
titleConfig={titleConfig}
|
|
subtitleSettings={{
|
|
enabled: subtitleSettings.enabled,
|
|
position: subtitleSettings.position,
|
|
font: subtitleSettings.font,
|
|
size: subtitleSettings.fontSize,
|
|
animation: subtitleSettings.animation,
|
|
}}
|
|
onClipSelect={clipOps.handleClipSelect}
|
|
onPlayPause={() => playback.setIsPlaying(!playback.isPlaying)}
|
|
/>
|
|
|
|
{/* 下半部:水平时间线 */}
|
|
<TimelinePanel
|
|
clips={clips}
|
|
selectedClipId={clipOps.selectedClipId}
|
|
currentMode={tpl.currentMode}
|
|
onClipSelect={clipOps.handleClipSelect}
|
|
onClipReorder={clipOps.handleClipReorder}
|
|
onClipRemove={clipOps.handleClipRemove}
|
|
onAddClip={clipOps.handleAddClip}
|
|
onClipTrim={clipOps.handleClipTrim}
|
|
onClipSplit={clipOps.handleClipSplit}
|
|
onClipResetTrim={clipOps.handleClipResetTrim}
|
|
currentTime={playback.currentTime}
|
|
pixelsPerSecond={playback.pixelsPerSecond}
|
|
onZoomChange={playback.handleZoomChange}
|
|
onSeek={playback.handleSeek}
|
|
totalDuration={totalDuration}
|
|
/>
|
|
</div>
|
|
|
|
{/* 右栏 260px:设置面板 */}
|
|
<RightPanel
|
|
rightTab={rightTab}
|
|
onTabChange={setRightTab}
|
|
selectedClip={clipOps.selectedClip}
|
|
subtitleSettings={subtitleSettings}
|
|
bgmSettings={bgmSettings}
|
|
clipsCount={clips.length}
|
|
totalDuration={totalDuration}
|
|
currentMode={tpl.currentMode}
|
|
onSubtitleSettingsChange={(partial) =>
|
|
setSubtitleSettings((prev) => ({ ...prev, ...partial }))
|
|
}
|
|
onBgmSettingsChange={(partial) => setBgmSettings((prev) => ({ ...prev, ...partial }))}
|
|
onClipUpdate={clipOps.handleClipUpdate}
|
|
onOpenBgmDrawer={() => drawers.setBgmDrawerOpen(true)}
|
|
onOpenSubtitleDrawer={() => drawers.setSubtitleDrawerOpen(true)}
|
|
voiceMaterials={voiceMaterials}
|
|
voiceMaterialsLoading={voiceMaterialsLoading}
|
|
onRefreshVoiceMaterials={refetchVoiceMaterials}
|
|
onClipVoiceSelect={clipOps.handleClipVoiceSelect}
|
|
onOpenTransitionDrawer={drawers.openTransitionDrawer}
|
|
onOpenSpeedDrawer={drawers.openSpeedDrawer}
|
|
onOpenTtsDrawer={drawers.openTtsDrawer}
|
|
onOpenWatermarkDrawer={() => drawers.setWatermarkDrawerOpen(true)}
|
|
onOpenIntroOutroDrawer={() => drawers.setIntroOutroDrawerOpen(true)}
|
|
onOpenPipDrawer={() => drawers.setPipDrawerOpen(true)}
|
|
onOpenFilterDrawer={() => drawers.setFilterDrawerOpen(true)}
|
|
onOpenGreenScreenDrawer={() => drawers.setChromaKeyDrawerOpen(true)}
|
|
onOpenStickerDrawer={() => drawers.setStickerDrawerOpen(true)}
|
|
clips={clips}
|
|
selectedClipId={clipOps.selectedClipId}
|
|
onClipSelect={clipOps.handleClipSelect}
|
|
onClipMoveUp={(clipId) => {
|
|
const idx = clips.findIndex((c) => c.id === clipId)
|
|
if (idx > 0) clipOps.handleClipReorder(idx, idx - 1)
|
|
}}
|
|
onClipMoveDown={(clipId) => {
|
|
const idx = clips.findIndex((c) => c.id === clipId)
|
|
if (idx < clips.length - 1) clipOps.handleClipReorder(idx, idx + 1)
|
|
}}
|
|
onClipRemove={clipOps.handleClipRemove}
|
|
onClipAdd={() => clipOps.handleAddClip("pip", 3)}
|
|
/>
|
|
</div>
|
|
|
|
{/* ═══ 第4行:底栏 40px ═══ */}
|
|
<StatusBar
|
|
clipsCount={clips.length}
|
|
totalDuration={totalDuration}
|
|
currentModeLabel={MODE_LABELS[tpl.currentMode]}
|
|
templateSegments={tpl.currentTemplate?.segments.length || 0}
|
|
/>
|
|
|
|
{/* ═══ 弹窗 ═══ */}
|
|
<SaveModal
|
|
open={tpl.saveModalOpen}
|
|
loading={tpl.saveLoading}
|
|
isUpdate={!!tpl.loadedTemplateId}
|
|
draftName={tpl.draftName}
|
|
draftCategory={tpl.draftCategory}
|
|
draftTags={tpl.draftTags}
|
|
categories={tpl.categories}
|
|
estimatedDuration={totalDuration}
|
|
onNameChange={tpl.setDraftName}
|
|
onCategoryChange={tpl.setDraftCategory}
|
|
onTagsChange={tpl.setDraftTags}
|
|
onSave={tpl.handleSave}
|
|
onCancel={() => tpl.setSaveModalOpen(false)}
|
|
/>
|
|
|
|
{/* ═══ Drawer 集合 ═══ */}
|
|
<EditorDrawers
|
|
bgmDrawerOpen={drawers.bgmDrawerOpen}
|
|
bgmSettings={bgmSettings}
|
|
onBgmSettingsChange={setBgmSettings}
|
|
onCloseBgmDrawer={() => drawers.setBgmDrawerOpen(false)}
|
|
subtitleDrawerOpen={drawers.subtitleDrawerOpen}
|
|
subtitleSettings={subtitleSettings}
|
|
onSubtitleSettingsChange={setSubtitleSettings}
|
|
onCloseSubtitleDrawer={() => drawers.setSubtitleDrawerOpen(false)}
|
|
transitionDrawerOpen={drawers.transitionDrawerOpen}
|
|
transitionTargetClipId={drawers.transitionTargetClipId}
|
|
clips={clips}
|
|
onTransitionChange={(config) =>
|
|
clipOps.handleTransitionChange(drawers.transitionTargetClipId, config)
|
|
}
|
|
onCloseTransitionDrawer={() => drawers.setTransitionDrawerOpen(false)}
|
|
speedDrawerOpen={drawers.speedDrawerOpen}
|
|
speedTargetClipId={drawers.speedTargetClipId}
|
|
onSpeedChange={(config) => clipOps.handleSpeedChange(drawers.speedTargetClipId, config)}
|
|
onApplySpeedAll={clipOps.handleApplySpeedAll}
|
|
onCloseSpeedDrawer={() => drawers.setSpeedDrawerOpen(false)}
|
|
ttsDrawerOpen={drawers.ttsDrawerOpen}
|
|
ttsTargetClipId={drawers.ttsTargetClipId}
|
|
onTtsChange={(config) => clipOps.handleTtsChange(drawers.ttsTargetClipId, config)}
|
|
onCloseTtsDrawer={() => drawers.setTtsDrawerOpen(false)}
|
|
watermarkDrawerOpen={drawers.watermarkDrawerOpen}
|
|
watermarkSettings={watermarkSettings}
|
|
onWatermarkChange={setWatermarkSettings}
|
|
onCloseWatermarkDrawer={() => drawers.setWatermarkDrawerOpen(false)}
|
|
introOutroDrawerOpen={drawers.introOutroDrawerOpen}
|
|
introOutroSettings={introOutroSettings}
|
|
onIntroOutroChange={setIntroOutroSettings}
|
|
onCloseIntroOutroDrawer={() => drawers.setIntroOutroDrawerOpen(false)}
|
|
pipDrawerOpen={drawers.pipDrawerOpen}
|
|
pipSettings={pipSettings}
|
|
totalDuration={totalDuration}
|
|
onPipChange={setPipSettings}
|
|
onClosePipDrawer={() => drawers.setPipDrawerOpen(false)}
|
|
filterDrawerOpen={drawers.filterDrawerOpen}
|
|
filterSettings={filterSettings}
|
|
onFilterChange={setFilterSettings}
|
|
onCloseFilterDrawer={() => drawers.setFilterDrawerOpen(false)}
|
|
chromaKeyDrawerOpen={drawers.chromaKeyDrawerOpen}
|
|
chromaKeySettings={chromaKeySettings}
|
|
onChromaKeyChange={setChromaKeySettings}
|
|
onCloseChromaKeyDrawer={() => drawers.setChromaKeyDrawerOpen(false)}
|
|
stickerDrawerOpen={drawers.stickerDrawerOpen}
|
|
stickerSettings={stickerSettings}
|
|
onStickerChange={setStickerSettings}
|
|
onCloseStickerDrawer={() => drawers.setStickerDrawerOpen(false)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default EditingPlanner
|