2025-09-19 18:08:03 +08:00

401 lines
11 KiB
TypeScript

/**
* 视频编辑功能状态管理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<VideoEditConfig>;
/** 编辑点变化回调 */
onEditPointsChange?: (editPoints: EditPoint[]) => void;
/** 编辑描述提交回调 */
onDescriptionSubmit?: (editPoint: EditPoint, description: string) => void;
}
/**
* useVideoEdit Hook返回值
*/
interface UseVideoEditReturn {
/** 编辑上下文 */
context: VideoEditContext;
/** 创建编辑点 */
createEditPoint: (position: EditPointPosition, timestamp: number) => Promise<EditPoint | null>;
/** 更新编辑点 */
updateEditPoint: (id: string, updates: Partial<EditPoint>) => Promise<boolean>;
/** 删除编辑点 */
deleteEditPoint: (id: string) => Promise<boolean>;
/** 选择编辑点 */
selectEditPoint: (id: string | null) => void;
/** 切换编辑模式 */
toggleEditMode: (enabled?: boolean) => void;
/** 显示/隐藏输入框 */
toggleInput: (id: string, show?: boolean) => void;
/** 提交编辑描述 */
submitDescription: (id: string, description: string) => Promise<boolean>;
/** 刷新编辑点列表 */
refreshEditPoints: () => Promise<void>;
/** 清空所有编辑点 */
clearAllEditPoints: () => Promise<boolean>;
}
/**
* 视频编辑状态管理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<EditPoint[]>([]);
const [isEditMode, setIsEditMode] = useState(true);
const [selectedEditPointId, setSelectedEditPointId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// 引用
const editPointsRef = useRef<EditPoint[]>([]);
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: currentVideo || null,
editPoints,
config: mergedConfig,
isEditMode,
selectedEditPointId
}), [currentVideo, editPoints, mergedConfig, isEditMode, selectedEditPointId]);
// 创建编辑点
const handleCreateEditPoint = useCallback(async (
position: EditPointPosition,
timestamp: number
): Promise<EditPoint | null> => {
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: EditPoint = {
...createdEditPoint,
showInput: true, // 确保新创建的编辑点显示输入框
connectionDirection: 'auto' as const
};
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<EditPoint>
): Promise<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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
};
}