diff --git a/apps/web/src/pages/editing-planner/hooks/useClipOperations.ts b/apps/web/src/pages/editing-planner/hooks/useClipOperations.ts deleted file mode 100644 index ad1dade67..000000000 --- a/apps/web/src/pages/editing-planner/hooks/useClipOperations.ts +++ /dev/null @@ -1,238 +0,0 @@ -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(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) => { - 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, - } -} diff --git a/apps/web/src/pages/editing-planner/hooks/useClipOperations/index.ts b/apps/web/src/pages/editing-planner/hooks/useClipOperations/index.ts new file mode 100644 index 000000000..4ea722310 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useClipOperations/index.ts @@ -0,0 +1,38 @@ +import type { ClipData } from "../../types" +import { useClipBasicOps } from "./useClipBasicOps" +import { useClipTrim } from "./useClipTrim" +import { useClipEffects } from "./useClipEffects" + +interface UseClipOperationsParams { + clips: ClipData[] + setClips: (updater: (prev: ClipData[]) => ClipData[]) => void +} + +/** + * 片段操作 Hook + * 片段增删改查、排序、裁剪、分割、转场、调速、TTS、配音选择 + */ +export const useClipOperations = ({ clips, setClips }: UseClipOperationsParams) => { + const basicOps = useClipBasicOps({ clips, setClips }) + const trimOps = useClipTrim({ setClips }) + const effectOps = useClipEffects({ setClips, handleClipUpdate: basicOps.handleClipUpdate }) + + return { + selectedClipId: basicOps.selectedClipId, + setSelectedClipId: basicOps.setSelectedClipId, + selectedClip: basicOps.selectedClip, + handleClipSelect: basicOps.handleClipSelect, + handleClipReorder: basicOps.handleClipReorder, + handleClipRemove: basicOps.handleClipRemove, + handleClipUpdate: basicOps.handleClipUpdate, + handleAddClip: basicOps.handleAddClip, + handleClipTrim: trimOps.handleClipTrim, + handleClipSplit: trimOps.handleClipSplit, + handleClipResetTrim: trimOps.handleClipResetTrim, + handleTransitionChange: effectOps.handleTransitionChange, + handleSpeedChange: effectOps.handleSpeedChange, + handleApplySpeedAll: effectOps.handleApplySpeedAll, + handleTtsChange: effectOps.handleTtsChange, + handleClipVoiceSelect: effectOps.handleClipVoiceSelect, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipBasicOps.ts b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipBasicOps.ts new file mode 100644 index 000000000..015541705 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipBasicOps.ts @@ -0,0 +1,86 @@ +import { useState, useMemo, useCallback } from "react" +import { Modal } from "antd" +import type { ClipData, ClipType } from "../../types" + +interface UseClipBasicOpsParams { + clips: ClipData[] + setClips: (updater: (prev: ClipData[]) => ClipData[]) => void +} + +/** + * 片段基础操作 Hook + * 选中状态、增删改查、重排 + */ +export function useClipBasicOps({ clips, setClips }: UseClipBasicOpsParams) { + const [selectedClipId, setSelectedClipId] = useState(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) => { + 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], + ) + + return { + selectedClipId, + setSelectedClipId, + selectedClip, + handleClipSelect, + handleClipReorder, + handleClipRemove, + handleClipUpdate, + handleAddClip, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipEffects.ts b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipEffects.ts new file mode 100644 index 000000000..f192d07c0 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipEffects.ts @@ -0,0 +1,74 @@ +import { useCallback } from "react" +import { message } from "antd" +import type { ClipData, TransitionConfig, SpeedConfig, TtsConfig } from "../../types" +import type { AssetItem } from "@/api/assets" + +interface UseClipEffectsParams { + setClips: (updater: (prev: ClipData[]) => ClipData[]) => void + handleClipUpdate: (clipId: string, data: Partial) => void +} + +/** + * 片段效果操作 Hook + * 转场、调速、TTS、配音选择 + */ +export function useClipEffects({ setClips, handleClipUpdate }: UseClipEffectsParams) { + 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 { + handleTransitionChange, + handleSpeedChange, + handleApplySpeedAll, + handleTtsChange, + handleClipVoiceSelect, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipTrim.ts b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipTrim.ts new file mode 100644 index 000000000..9272d627c --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipTrim.ts @@ -0,0 +1,89 @@ +import { useCallback } from "react" +import type { ClipData, TrimConfig } from "../../types" + +interface UseClipTrimParams { + setClips: (updater: (prev: ClipData[]) => ClipData[]) => void +} + +/** + * 片段裁剪分割 Hook + * 裁剪、分割、重置裁剪 + */ +export function useClipTrim({ setClips }: UseClipTrimParams) { + 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], + ) + + return { + handleClipTrim, + handleClipSplit, + handleClipResetTrim, + } +}