From d79a652c69278eabd2ef8f894a04bf6190e869a6 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 13:54:51 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editing-planner):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?useTimelineMenus=E4=B8=BA=E5=AD=90Hook=EF=BC=88192=E2=86=9267?= =?UTF-8?q?=E8=A1=8C,=20-65%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/timeline-menus/useAddPicker.ts | 116 ++++++++++++ .../hooks/timeline-menus/useContextMenu.ts | 68 +++++++ .../editing-planner/hooks/useTimelineMenus.ts | 173 +++--------------- 3 files changed, 208 insertions(+), 149 deletions(-) create mode 100755 apps/web/src/pages/editing-planner/hooks/timeline-menus/useAddPicker.ts create mode 100755 apps/web/src/pages/editing-planner/hooks/timeline-menus/useContextMenu.ts mode change 100644 => 100755 apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts diff --git a/apps/web/src/pages/editing-planner/hooks/timeline-menus/useAddPicker.ts b/apps/web/src/pages/editing-planner/hooks/timeline-menus/useAddPicker.ts new file mode 100755 index 000000000..cae564b25 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/timeline-menus/useAddPicker.ts @@ -0,0 +1,116 @@ +import { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect } from "react" +import type { ClipType } from "../../types" +import { DEFAULT_ADD_DURATION, ADD_PICKER_WIDTH, TRACK_GAP } from "../../constants/timeline" + +interface UseAddPickerOptions { + currentMode: string + onAddClip: (type: ClipType, duration: number) => void +} + +export function useAddPicker({ currentMode, onAddClip }: UseAddPickerOptions) { + const [showAddPicker, setShowAddPicker] = useState(false) + const pickerRef = useRef(null) + const addCardRef = useRef(null) + const [pickerPos, setPickerPos] = useState<{ top: number; right: number }>({ top: 0, right: 0 }) + + const availableTypes: ClipType[] = useMemo( + () => + currentMode === "voice_over" ? ["voice"] : currentMode === "pip" ? ["pip"] : ["voice", "pip"], + [currentMode], + ) + + const defaultAddType: ClipType = useMemo(() => { + if (currentMode === "voice_over") return "voice" + if (currentMode === "pip") return "pip" + return "voice" + }, [currentMode]) + + const [addType, setAddType] = useState(defaultAddType) + const [addDuration, setAddDuration] = useState(DEFAULT_ADD_DURATION) + + useEffect(() => { + if (!availableTypes.includes(addType)) { + setAddType(defaultAddType) + } + }, [currentMode, addType, availableTypes, defaultAddType]) + + const updatePickerPosition = useCallback(() => { + if (!addCardRef.current) return + const rect = addCardRef.current.getBoundingClientRect() + const vw = window.innerWidth + const roughHeight = 180 + let top = rect.top - TRACK_GAP - roughHeight + if (top < 8) top = 8 + let right = vw - rect.right + if (rect.right - ADD_PICKER_WIDTH < 8) { + right = vw - ADD_PICKER_WIDTH - 8 + } + setPickerPos({ top, right }) + }, []) + + const handleTogglePicker = useCallback(() => { + if (!showAddPicker) { + setAddType(defaultAddType) + updatePickerPosition() + } + setShowAddPicker((v) => !v) + }, [showAddPicker, defaultAddType, updatePickerPosition]) + + useLayoutEffect(() => { + if (!showAddPicker || !pickerRef.current || !addCardRef.current) return + const pickerEl = pickerRef.current + const addRect = addCardRef.current.getBoundingClientRect() + const pickerH = pickerEl.offsetHeight + const vh = window.innerHeight + const vw = window.innerWidth + + let top = addRect.top - TRACK_GAP - pickerH + if (top < 8) { + top = addRect.bottom + TRACK_GAP + if (top + pickerH > vh - 8) { + top = vh - 8 - pickerH + if (top < 8) top = 8 + } + } + + let right = vw - addRect.right + const pickerRect = pickerEl.getBoundingClientRect() + if (pickerRect.left < 8) { + right = vw - ADD_PICKER_WIDTH - 8 + } + + setPickerPos({ top, right }) + }, [showAddPicker]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) { + setShowAddPicker(false) + } + } + if (showAddPicker) { + document.addEventListener("mousedown", handleClickOutside) + } + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [showAddPicker]) + + const handleConfirmAdd = useCallback(() => { + onAddClip(addType, addDuration) + setShowAddPicker(false) + }, [onAddClip, addType, addDuration]) + + return { + showAddPicker, + setShowAddPicker, + pickerRef, + addCardRef, + pickerPos, + availableTypes, + addType, + addDuration, + setAddType, + setAddDuration, + handleTogglePicker, + handleConfirmAdd, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/timeline-menus/useContextMenu.ts b/apps/web/src/pages/editing-planner/hooks/timeline-menus/useContextMenu.ts new file mode 100755 index 000000000..6564354f9 --- /dev/null +++ b/apps/web/src/pages/editing-planner/hooks/timeline-menus/useContextMenu.ts @@ -0,0 +1,68 @@ +import { useState, useRef, useCallback, useEffect } from "react" + +interface ContextMenuState { + x: number + y: number + clipId: string +} + +interface UseContextMenuOptions { + onClipSplit?: (clipId: string, splitRatio: number) => void + onClipResetTrim?: (clipId: string) => void + onClipRemove?: (clipId: string) => void +} + +export function useContextMenu({ + onClipSplit, + onClipResetTrim, + onClipRemove, +}: UseContextMenuOptions) { + const [contextMenu, setContextMenu] = useState(null) + const contextMenuRef = useRef(null) + + const handleContextMenu = useCallback((e: React.MouseEvent, clipId: string) => { + e.preventDefault() + e.stopPropagation() + setContextMenu({ x: e.clientX, y: e.clientY, clipId }) + }, []) + + const handleContextSplit = useCallback(() => { + if (!contextMenu) return + onClipSplit?.(contextMenu.clipId, 0.5) + setContextMenu(null) + }, [contextMenu, onClipSplit]) + + const handleContextResetTrim = useCallback(() => { + if (!contextMenu) return + onClipResetTrim?.(contextMenu.clipId) + setContextMenu(null) + }, [contextMenu, onClipResetTrim]) + + const handleContextDelete = useCallback(() => { + if (!contextMenu) return + onClipRemove?.(contextMenu.clipId) + setContextMenu(null) + }, [contextMenu, onClipRemove]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { + setContextMenu(null) + } + } + if (contextMenu) { + document.addEventListener("mousedown", handleClickOutside) + } + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [contextMenu]) + + return { + contextMenu, + setContextMenu, + contextMenuRef, + handleContextMenu, + handleContextSplit, + handleContextResetTrim, + handleContextDelete, + } +} diff --git a/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts b/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts old mode 100644 new mode 100755 index a2b45d5c0..ca6ba98e1 --- a/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts +++ b/apps/web/src/pages/editing-planner/hooks/useTimelineMenus.ts @@ -1,12 +1,7 @@ -import { useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect } from "react" +import { useState } from "react" import type { ClipData, ClipType } from "../types" -import { DEFAULT_ADD_DURATION, ADD_PICKER_WIDTH, TRACK_GAP } from "../constants/timeline" - -interface ContextMenuState { - x: number - y: number - clipId: string -} +import { useContextMenu } from "./timeline-menus/useContextMenu" +import { useAddPicker } from "./timeline-menus/useAddPicker" /** * 时间线菜单 Hook @@ -20,150 +15,30 @@ export const useTimelineMenus = ( onClipResetTrim?: (clipId: string) => void, onClipRemove?: (clipId: string) => void, ) => { - /* ── 右键菜单 ── */ - const [contextMenu, setContextMenu] = useState(null) - const contextMenuRef = useRef(null) - - /* ── 添加片段面板 ── */ - const [showAddPicker, setShowAddPicker] = useState(false) - const pickerRef = useRef(null) - const addCardRef = useRef(null) - const [pickerPos, setPickerPos] = useState<{ top: number; right: number }>({ top: 0, right: 0 }) - - /* ── 悬停的片段 ID(显示裁剪手柄) ── */ const [hoveredClipId, setHoveredClipId] = useState(null) - /* ── 根据模式决定可选类型 ── */ - const availableTypes: ClipType[] = useMemo( - () => - currentMode === "voice_over" ? ["voice"] : currentMode === "pip" ? ["pip"] : ["voice", "pip"], - [currentMode], - ) + const { + contextMenu, + contextMenuRef, + handleContextMenu, + handleContextSplit, + handleContextResetTrim, + handleContextDelete, + } = useContextMenu({ onClipSplit, onClipResetTrim, onClipRemove }) - /* ── 默认添加类型:跟随模式 ── */ - const defaultAddType: ClipType = useMemo(() => { - if (currentMode === "voice_over") return "voice" - if (currentMode === "pip") return "pip" - return "voice" - }, [currentMode]) - - /* ── "+" 卡片:类型+时长选择状态 ── */ - const [addType, setAddType] = useState(defaultAddType) - const [addDuration, setAddDuration] = useState(DEFAULT_ADD_DURATION) - - /* ── 模式切换时自动同步默认添加类型 ── */ - useEffect(() => { - if (!availableTypes.includes(addType)) { - setAddType(defaultAddType) - } - }, [currentMode, addType, availableTypes, defaultAddType]) - - /* ── 计算 picker 初始位置 ── */ - const updatePickerPosition = useCallback(() => { - if (!addCardRef.current) return - const rect = addCardRef.current.getBoundingClientRect() - const vw = window.innerWidth - const roughHeight = 180 - let top = rect.top - TRACK_GAP - roughHeight - if (top < 8) top = 8 - let right = vw - rect.right - if (rect.right - ADD_PICKER_WIDTH < 8) { - right = vw - ADD_PICKER_WIDTH - 8 - } - setPickerPos({ top, right }) - }, []) - - const handleTogglePicker = useCallback(() => { - if (!showAddPicker) { - setAddType(defaultAddType) - updatePickerPosition() - } - setShowAddPicker((v) => !v) - }, [showAddPicker, defaultAddType, updatePickerPosition]) - - /* ── 渲染后精确边界校正 ── */ - useLayoutEffect(() => { - if (!showAddPicker || !pickerRef.current || !addCardRef.current) return - const pickerEl = pickerRef.current - const addRect = addCardRef.current.getBoundingClientRect() - const pickerH = pickerEl.offsetHeight - const vh = window.innerHeight - const vw = window.innerWidth - - let top = addRect.top - TRACK_GAP - pickerH - if (top < 8) { - top = addRect.bottom + TRACK_GAP - if (top + pickerH > vh - 8) { - top = vh - 8 - pickerH - if (top < 8) top = 8 - } - } - - let right = vw - addRect.right - const pickerRect = pickerEl.getBoundingClientRect() - if (pickerRect.left < 8) { - right = vw - ADD_PICKER_WIDTH - 8 - } - - setPickerPos({ top, right }) - }, [showAddPicker]) - - /* ── 点击外部关闭添加面板 ── */ - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) { - setShowAddPicker(false) - } - } - if (showAddPicker) { - document.addEventListener("mousedown", handleClickOutside) - } - return () => document.removeEventListener("mousedown", handleClickOutside) - }, [showAddPicker]) - - /* ── 点击外部关闭右键菜单 ── */ - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { - setContextMenu(null) - } - } - if (contextMenu) { - document.addEventListener("mousedown", handleClickOutside) - } - return () => document.removeEventListener("mousedown", handleClickOutside) - }, [contextMenu]) - - /* ── 确认添加片段 ── */ - const handleConfirmAdd = useCallback(() => { - onAddClip(addType, addDuration) - setShowAddPicker(false) - }, [onAddClip, addType, addDuration]) - - /* ── 右键菜单操作 ── */ - const handleContextMenu = useCallback((e: React.MouseEvent, clipId: string) => { - e.preventDefault() - e.stopPropagation() - setContextMenu({ x: e.clientX, y: e.clientY, clipId }) - }, []) - - const handleContextSplit = useCallback(() => { - if (!contextMenu) return - onClipSplit?.(contextMenu.clipId, 0.5) - setContextMenu(null) - }, [contextMenu, onClipSplit]) - - const handleContextResetTrim = useCallback(() => { - if (!contextMenu) return - onClipResetTrim?.(contextMenu.clipId) - setContextMenu(null) - }, [contextMenu, onClipResetTrim]) - - const handleContextDelete = useCallback(() => { - if (!contextMenu) return - onClipRemove?.(contextMenu.clipId) - setContextMenu(null) - }, [contextMenu, onClipRemove]) + const { + showAddPicker, + pickerRef, + addCardRef, + pickerPos, + availableTypes, + addType, + addDuration, + setAddType, + setAddDuration, + handleTogglePicker, + handleConfirmAdd, + } = useAddPicker({ currentMode, onAddClip }) return { // 右键菜单 -- 2.54.0