a1bda3e484
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 32s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 47s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m16s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 57s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 15s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 54s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m3s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 28s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m36s
Extract 4 business hooks from EditingPlanner.tsx: - useEditorDrawers (78行): 11个抽屉开关 + 目标ID + 快捷打开 - usePlaybackControl (56行): 播放/暂停、rAF帧推进、缩放、seek - useClipOperations (231行): 片段CRUD、重排、裁剪、分割、转场、调速、TTS、配音 - useTemplateManagement (459行): 模板列表/详情/计划加载、保存、模式切换、筛选搜索 主文件: 997→415行(-58%)
239 lines
6.3 KiB
TypeScript
239 lines
6.3 KiB
TypeScript
import { useState, useCallback, useMemo } from "react"
|
|
import { Modal, message } from "antd"
|
|
import type {
|
|
ClipData,
|
|
ClipType,
|
|
TransitionConfig,
|
|
SpeedConfig,
|
|
TtsConfig,
|
|
TrimConfig,
|
|
} from "../types"
|
|
import type { AssetItem } from "@/api/assets"
|
|
|
|
interface UseClipOperationsParams {
|
|
clips: ClipData[]
|
|
setClips: (updater: (prev: ClipData[]) => ClipData[]) => void
|
|
}
|
|
|
|
/**
|
|
* 片段操作 Hook
|
|
* 片段增删改查、排序、裁剪、分割、转场、调速、TTS、配音选择
|
|
*/
|
|
export const useClipOperations = ({ clips, setClips }: UseClipOperationsParams) => {
|
|
const [selectedClipId, setSelectedClipId] = useState<string | null>(null)
|
|
|
|
const selectedClip = useMemo(
|
|
() => clips.find((c) => c.id === selectedClipId) || null,
|
|
[clips, selectedClipId],
|
|
)
|
|
|
|
/* ── 选中 / 重排 / 删除 ── */
|
|
|
|
const handleClipSelect = useCallback((clipId: string) => {
|
|
setSelectedClipId(clipId)
|
|
}, [])
|
|
|
|
const handleClipReorder = useCallback(
|
|
(fromIdx: number, toIdx: number) => {
|
|
setClips((prev) => {
|
|
const updated = [...prev]
|
|
const [moved] = updated.splice(fromIdx, 1)
|
|
updated.splice(toIdx, 0, moved)
|
|
return updated.map((c, i) => ({ ...c, order: i }))
|
|
})
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
const handleClipRemove = useCallback(
|
|
(clipId: string) => {
|
|
Modal.confirm({
|
|
title: "删除片段",
|
|
content: "确定要删除这个片段吗?此操作可通过撤销恢复。",
|
|
okText: "删除",
|
|
okType: "danger",
|
|
cancelText: "取消",
|
|
onOk: () => {
|
|
setClips((prev) => prev.filter((c) => c.id !== clipId))
|
|
if (selectedClipId === clipId) setSelectedClipId(null)
|
|
},
|
|
})
|
|
},
|
|
[setClips, selectedClipId],
|
|
)
|
|
|
|
const handleClipUpdate = useCallback(
|
|
(clipId: string, data: Partial<ClipData>) => {
|
|
setClips((prev) => prev.map((c) => (c.id === clipId ? { ...c, ...data } : c)))
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
/**
|
|
* 添加片段(不绑定任何素材)
|
|
* 片段 = 时间规划 + 类型标记
|
|
*/
|
|
const handleAddClip = useCallback(
|
|
(type: ClipType, duration: number) => {
|
|
const newClip: ClipData = {
|
|
id: `clip-${Date.now()}`,
|
|
type,
|
|
duration,
|
|
startOffset: 0,
|
|
order: clips.length,
|
|
}
|
|
setClips((prev) => [...prev, newClip])
|
|
},
|
|
[clips.length, setClips],
|
|
)
|
|
|
|
/* ── 裁剪 / 分割 / 重置 ── */
|
|
|
|
const handleClipTrim = useCallback(
|
|
(clipId: string, trimConfig: TrimConfig, newDuration: number) => {
|
|
setClips((prev) =>
|
|
prev.map((c) =>
|
|
c.id === clipId ? { ...c, trim_config: trimConfig, duration: newDuration } : c,
|
|
),
|
|
)
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
const handleClipSplit = useCallback(
|
|
(clipId: string, splitRatio: number) => {
|
|
setClips((prev) => {
|
|
const idx = prev.findIndex((c) => c.id === clipId)
|
|
if (idx === -1) return prev
|
|
const clip = prev[idx]
|
|
const splitPoint = Math.round(clip.duration * splitRatio * 10) / 10
|
|
if (splitPoint < 0.5 || splitPoint >= clip.duration - 0.5) return prev
|
|
|
|
// 前半段
|
|
const firstHalf: ClipData = {
|
|
...clip,
|
|
duration: splitPoint,
|
|
trim_config: clip.trim_config
|
|
? {
|
|
...clip.trim_config,
|
|
end_time: clip.trim_config.start_time + splitPoint,
|
|
}
|
|
: undefined,
|
|
}
|
|
|
|
// 后半段
|
|
const secondHalf: ClipData = {
|
|
...clip,
|
|
id: `clip-${Date.now()}`,
|
|
duration: clip.duration - splitPoint,
|
|
startOffset: clip.startOffset + splitPoint,
|
|
trim_config: clip.trim_config
|
|
? {
|
|
...clip.trim_config,
|
|
start_time: clip.trim_config.start_time + splitPoint,
|
|
}
|
|
: undefined,
|
|
order: (clip.order ?? idx) + 1,
|
|
}
|
|
|
|
const updated = [...prev]
|
|
updated[idx] = firstHalf
|
|
updated.splice(idx + 1, 0, secondHalf)
|
|
return updated.map((c, i) => ({ ...c, order: i }))
|
|
})
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
const handleClipResetTrim = useCallback(
|
|
(clipId: string) => {
|
|
setClips((prev) =>
|
|
prev.map((c) => {
|
|
if (c.id !== clipId || !c.trim_config) return c
|
|
const originalDuration = c.trim_config.original_duration ?? c.duration
|
|
return {
|
|
...c,
|
|
duration: originalDuration,
|
|
trim_config: undefined,
|
|
}
|
|
}),
|
|
)
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
/* ── 转场 / 调速 / TTS ── */
|
|
|
|
const handleTransitionChange = useCallback(
|
|
(targetClipId: string | null, config: TransitionConfig) => {
|
|
if (targetClipId) {
|
|
handleClipUpdate(targetClipId, { transition: config })
|
|
}
|
|
// 同时更新全局默认转场(供新片段使用)—— 暂未实现全局默认
|
|
},
|
|
[handleClipUpdate],
|
|
)
|
|
|
|
const handleSpeedChange = useCallback(
|
|
(targetClipId: string | null, config: SpeedConfig) => {
|
|
if (targetClipId) {
|
|
handleClipUpdate(targetClipId, { speed: config })
|
|
}
|
|
},
|
|
[handleClipUpdate],
|
|
)
|
|
|
|
const handleApplySpeedAll = useCallback(
|
|
(config: SpeedConfig) => {
|
|
setClips((prev) => prev.map((c) => ({ ...c, speed: { ...config } })))
|
|
message.success("已应用到所有片段")
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
const handleTtsChange = useCallback(
|
|
(targetClipId: string | null, ttsConfig: TtsConfig) => {
|
|
if (!targetClipId) return
|
|
handleClipUpdate(targetClipId, { tts_config: ttsConfig })
|
|
},
|
|
[handleClipUpdate],
|
|
)
|
|
|
|
/** 为片段选择配音素材 */
|
|
const handleClipVoiceSelect = useCallback(
|
|
(clipId: string, asset: AssetItem | null) => {
|
|
setClips((prev) =>
|
|
prev.map((c) =>
|
|
c.id === clipId
|
|
? {
|
|
...c,
|
|
voice_asset_id: asset?.id ?? undefined,
|
|
voice_file_url: asset?.file_url ?? undefined,
|
|
}
|
|
: c,
|
|
),
|
|
)
|
|
},
|
|
[setClips],
|
|
)
|
|
|
|
return {
|
|
selectedClipId,
|
|
setSelectedClipId,
|
|
selectedClip,
|
|
handleClipSelect,
|
|
handleClipReorder,
|
|
handleClipRemove,
|
|
handleClipUpdate,
|
|
handleAddClip,
|
|
handleClipTrim,
|
|
handleClipSplit,
|
|
handleClipResetTrim,
|
|
handleTransitionChange,
|
|
handleSpeedChange,
|
|
handleApplySpeedAll,
|
|
handleTtsChange,
|
|
handleClipVoiceSelect,
|
|
}
|
|
}
|