forked from 77media/video-flow
401 lines
11 KiB
TypeScript
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
|
|
};
|
|
}
|