From 526591b98f6004dbf7d5cb915c83400744f19637 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 07:26:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editing-planner):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=20useClipOperations=20Hook=EF=BC=8C=E6=8C=89=E8=81=8C=E8=B4=A3?= =?UTF-8?q?=E6=8B=86=E4=B8=BA=203=20=E4=B8=AA=E5=AD=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主文件 238→60 行(-75%),拆分为: - useClipBasicOps: 选中状态 + 增删改查重排 - useClipTrim: 裁剪、分割、重置裁剪 - useClipEffects: 转场、调速、TTS、配音选择 导入路径保持向后兼容 --- .../hooks/useClipOperations/index.ts | 38 ++++++++ .../useClipOperations/useClipBasicOps.ts | 86 ++++++++++++++++++ .../hooks/useClipOperations/useClipEffects.ts | 74 +++++++++++++++ .../hooks/useClipOperations/useClipTrim.ts | 89 +++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/hooks/useClipOperations/index.ts create mode 100644 apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipBasicOps.ts create mode 100644 apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipEffects.ts create mode 100644 apps/web/src/pages/editing-planner/hooks/useClipOperations/useClipTrim.ts 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..585256b29 --- /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..cac718cb9 --- /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..fc394a456 --- /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..2a48f0ff4 --- /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, + } +}