/** * 视频编辑功能状态管理Hook */ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { EditPoint, EditPointAction, VideoEditContext, VideoEditConfig, CreateEditPointRequest, UpdateEditPointRequest, DeleteEditPointRequest, EditPointStatus, EditPointPosition } from './types'; import { createEditPoint, updateEditPoint, deleteEditPoint, getEditPoints } from './api'; import { debounce } from './utils'; /** * 默认编辑配置 */ const DEFAULT_CONFIG: VideoEditConfig = { enabled: true, maxEditPoints: 10, connectionStyle: { color: 'rgba(255, 255, 255, 0.8)', strokeWidth: 2, dashArray: '5,5' }, pointStyle: { size: 12, color: '#3b82f6', pulseColor: 'rgba(59, 130, 246, 0.3)' } }; /** * useVideoEdit Hook参数 */ interface UseVideoEditProps { /** 项目ID */ projectId: string; /** 用户ID */ userId: number; /** 当前视频信息 */ currentVideo?: { id: string; url: string; duration: number; } | null; /** 编辑配置 */ config?: Partial; /** 编辑点变化回调 */ onEditPointsChange?: (editPoints: EditPoint[]) => void; /** 编辑描述提交回调 */ onDescriptionSubmit?: (editPoint: EditPoint, description: string) => void; } /** * useVideoEdit Hook返回值 */ interface UseVideoEditReturn { /** 编辑上下文 */ context: VideoEditContext; /** 创建编辑点 */ createEditPoint: (position: EditPointPosition, timestamp: number) => Promise; /** 更新编辑点 */ updateEditPoint: (id: string, updates: Partial) => Promise; /** 删除编辑点 */ deleteEditPoint: (id: string) => Promise; /** 选择编辑点 */ selectEditPoint: (id: string | null) => void; /** 切换编辑模式 */ toggleEditMode: (enabled?: boolean) => void; /** 显示/隐藏输入框 */ toggleInput: (id: string, show?: boolean) => void; /** 提交编辑描述 */ submitDescription: (id: string, description: string) => Promise; /** 刷新编辑点列表 */ refreshEditPoints: () => Promise; /** 清空所有编辑点 */ clearAllEditPoints: () => Promise; } /** * 视频编辑状态管理Hook */ export function useVideoEdit({ projectId, userId, currentVideo, config = {}, onEditPointsChange, onDescriptionSubmit }: UseVideoEditProps): UseVideoEditReturn { // 合并配置 const mergedConfig = useMemo(() => ({ ...DEFAULT_CONFIG, ...config, connectionStyle: { ...DEFAULT_CONFIG.connectionStyle, ...config.connectionStyle }, pointStyle: { ...DEFAULT_CONFIG.pointStyle, ...config.pointStyle } }), [config]); // 状态管理 const [editPoints, setEditPoints] = useState([]); const [isEditMode, setIsEditMode] = useState(true); const [selectedEditPointId, setSelectedEditPointId] = useState(null); const [isLoading, setIsLoading] = useState(false); // 引用 const editPointsRef = useRef([]); const nextIdRef = useRef(1); // 同步editPoints到ref useEffect(() => { editPointsRef.current = editPoints; }, [editPoints]); // 生成唯一ID const generateId = useCallback(() => { return `edit-point-${Date.now()}-${nextIdRef.current++}`; }, []); // 创建编辑上下文 const context: VideoEditContext = useMemo(() => ({ currentVideo, editPoints, config: mergedConfig, isEditMode, selectedEditPointId }), [currentVideo, editPoints, mergedConfig, isEditMode, selectedEditPointId]); // 创建编辑点 const handleCreateEditPoint = useCallback(async ( position: EditPointPosition, timestamp: number ): Promise => { if (!currentVideo || editPoints.length >= mergedConfig.maxEditPoints) { return null; } const newEditPoint: EditPoint = { id: generateId(), videoId: currentVideo.id, projectId, userId, position, timestamp, description: '', status: EditPointStatus.PENDING, createdAt: Date.now(), updatedAt: Date.now(), showInput: true, connectionDirection: 'auto' }; try { setIsLoading(true); // 先更新本地状态,提供即时反馈 setEditPoints(prev => [...prev, newEditPoint]); setSelectedEditPointId(newEditPoint.id); // 调用API创建编辑点 const createdEditPoint = await createEditPoint({ videoId: currentVideo.id, projectId, position, timestamp, description: '' }); if (createdEditPoint) { // 更新为服务器返回的数据,但保持输入框显示状态 const updatedEditPoint = { ...createdEditPoint, showInput: true, // 确保新创建的编辑点显示输入框 connectionDirection: 'auto' }; setEditPoints(prev => prev.map(point => point.id === newEditPoint.id ? updatedEditPoint : point ) ); onEditPointsChange?.(editPointsRef.current); return updatedEditPoint; } else { // 创建失败,移除本地添加的点 setEditPoints(prev => prev.filter(point => point.id !== newEditPoint.id)); return null; } } catch (error) { console.error('创建编辑点失败:', error); // 创建失败,移除本地添加的点 setEditPoints(prev => prev.filter(point => point.id !== newEditPoint.id)); return null; } finally { setIsLoading(false); } }, [currentVideo, editPoints.length, mergedConfig.maxEditPoints, generateId, projectId, userId, onEditPointsChange]); // 更新编辑点 const handleUpdateEditPoint = useCallback(async ( id: string, updates: Partial ): Promise => { try { setIsLoading(true); // 先更新本地状态 setEditPoints(prev => prev.map(point => point.id === id ? { ...point, ...updates, updatedAt: Date.now() } : point ) ); // 调用API更新 const success = await updateEditPoint({ id, description: updates.description, status: updates.status, position: updates.position }); if (success) { onEditPointsChange?.(editPointsRef.current); return true; } else { // 更新失败,恢复原状态 await refreshEditPoints(); return false; } } catch (error) { console.error('更新编辑点失败:', error); // 更新失败,恢复原状态 await refreshEditPoints(); return false; } finally { setIsLoading(false); } }, [onEditPointsChange]); // 删除编辑点 const handleDeleteEditPoint = useCallback(async (id: string): Promise => { try { setIsLoading(true); // 先更新本地状态 const originalEditPoints = editPointsRef.current; setEditPoints(prev => prev.filter(point => point.id !== id)); if (selectedEditPointId === id) { setSelectedEditPointId(null); } // 调用API删除 const success = await deleteEditPoint({ id }); if (success) { onEditPointsChange?.(editPointsRef.current); return true; } else { // 删除失败,恢复原状态 setEditPoints(originalEditPoints); return false; } } catch (error) { console.error('删除编辑点失败:', error); // 删除失败,恢复原状态 await refreshEditPoints(); return false; } finally { setIsLoading(false); } }, [selectedEditPointId, onEditPointsChange]); // 选择编辑点 const selectEditPoint = useCallback((id: string | null) => { setSelectedEditPointId(id); }, []); // 切换编辑模式 const toggleEditMode = useCallback((enabled?: boolean) => { setIsEditMode(prev => enabled !== undefined ? enabled : !prev); if (enabled === false) { setSelectedEditPointId(null); } }, []); // 显示/隐藏输入框 const toggleInput = useCallback((id: string, show?: boolean) => { setEditPoints(prev => prev.map(point => point.id === id ? { ...point, showInput: show !== undefined ? show : !point.showInput } : point ) ); }, []); // 提交编辑描述 const submitDescription = useCallback(async (id: string, description: string): Promise => { const editPoint = editPoints.find(point => point.id === id); if (!editPoint) return false; // 只更新描述和状态,不隐藏输入框(由调用方决定) const success = await handleUpdateEditPoint(id, { description, status: EditPointStatus.EDITED }); if (success && onDescriptionSubmit) { const updatedEditPoint = editPointsRef.current.find(point => point.id === id); if (updatedEditPoint) { onDescriptionSubmit(updatedEditPoint, description); } } return success; }, [editPoints, handleUpdateEditPoint, onDescriptionSubmit]); // 刷新编辑点列表 const refreshEditPoints = useCallback(async () => { if (!currentVideo) return; try { setIsLoading(true); const response = await getEditPoints({ videoId: currentVideo.id, projectId }); if (response) { setEditPoints(response.editPoints); onEditPointsChange?.(response.editPoints); } } catch (error) { console.error('刷新编辑点列表失败:', error); } finally { setIsLoading(false); } }, [currentVideo, projectId, onEditPointsChange]); // 清空所有编辑点 const clearAllEditPoints = useCallback(async (): Promise => { try { setIsLoading(true); // 批量删除所有编辑点 const deletePromises = editPoints.map(point => deleteEditPoint({ id: point.id })); const results = await Promise.all(deletePromises); const allSuccess = results.every(result => result); if (allSuccess) { setEditPoints([]); setSelectedEditPointId(null); onEditPointsChange?.([]); return true; } else { // 部分删除失败,刷新列表 await refreshEditPoints(); return false; } } catch (error) { console.error('清空编辑点失败:', error); return false; } finally { setIsLoading(false); } }, [editPoints, refreshEditPoints, onEditPointsChange]); // 当视频变化时,刷新编辑点列表 useEffect(() => { if (currentVideo) { refreshEditPoints(); } else { setEditPoints([]); setSelectedEditPointId(null); } }, [currentVideo?.id, refreshEditPoints]); return { context, createEditPoint: handleCreateEditPoint, updateEditPoint: handleUpdateEditPoint, deleteEditPoint: handleDeleteEditPoint, selectEditPoint, toggleEditMode, toggleInput, submitDescription, refreshEditPoints, clearAllEditPoints }; }