视频修改功能开发

This commit is contained in:
qikongjian 2025-09-22 20:03:47 +08:00
parent 89255f9feb
commit ca4408f5df
9 changed files with 243 additions and 438 deletions

View File

@ -20,8 +20,7 @@ import { showEditingNotification } from "@/components/pages/work-flow/editing-no
// import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; // import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
import { exportVideoWithRetry } from '@/utils/export-service'; import { exportVideoWithRetry } from '@/utils/export-service';
import { getFirstFrame } from '@/utils/tools'; import { getFirstFrame } from '@/utils/tools';
// 临时禁用视频编辑功能 import { EditPoint as EditPointType } from './work-flow/video-edit/types';
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
import { useDeviceType } from '@/hooks/useDeviceType'; import { useDeviceType } from '@/hooks/useDeviceType';
import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast'; import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast';
@ -303,9 +302,8 @@ const WorkFlow = React.memo(function WorkFlow() {
// setAiEditingInProgress(false); // 已移除该状态变量 // setAiEditingInProgress(false); // 已移除该状态变量
}, []); }, []);
// 临时禁用视频编辑功能
// 视频编辑描述提交处理函数 // 视频编辑描述提交处理函数
/*const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => { const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => {
console.log('🎬 视频编辑描述提交:', { editPoint, description }); console.log('🎬 视频编辑描述提交:', { editPoint, description });
// 构造编辑消息发送到SmartChatBox // 构造编辑消息发送到SmartChatBox
@ -333,7 +331,7 @@ Please process this video editing request.`;
description: `Your edit request for timestamp ${Math.floor(editPoint.timestamp)}s has been submitted successfully.`, description: `Your edit request for timestamp ${Math.floor(editPoint.timestamp)}s has been submitted successfully.`,
duration: 3 duration: 3
}); });
}, [currentSketchIndex, isSmartChatBoxOpen]);*/ }, [currentSketchIndex, isSmartChatBoxOpen]);
// 测试导出接口的处理函数(使用封装的导出服务) // 测试导出接口的处理函数(使用封装的导出服务)
const handleTestExport = useCallback(async () => { const handleTestExport = useCallback(async () => {
@ -520,7 +518,9 @@ Please process this video editing request.`;
isSmartChatBoxOpen={isSmartChatBoxOpen} isSmartChatBoxOpen={isSmartChatBoxOpen}
onRetryVideo={(video_id) => handleRetryVideo(video_id)} onRetryVideo={(video_id) => handleRetryVideo(video_id)}
onSelectView={(view) => setSelectedView(view)} onSelectView={(view) => setSelectedView(view)}
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit} enableVideoEdit={true}
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
projectId={episodeId}
/> />
)} )}
</div> </div>

View File

@ -43,6 +43,12 @@ interface H5MediaViewerProps {
onRetryVideo?: (video_id: string) => void; onRetryVideo?: (video_id: string) => void;
/** 切换选择视图final 或 video */ /** 切换选择视图final 或 video */
onSelectView?: (view: 'final' | 'video') => void; onSelectView?: (view: 'final' | 'video') => void;
/** 启用视频编辑功能 */
enableVideoEdit?: boolean;
/** 视频编辑描述提交回调 */
onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void;
/** 项目ID */
projectId?: string;
} }
/** /**
@ -67,7 +73,10 @@ export function H5MediaViewer({
onGotoCut, onGotoCut,
isSmartChatBoxOpen, isSmartChatBoxOpen,
onRetryVideo, onRetryVideo,
onSelectView onSelectView,
enableVideoEdit,
onVideoEditDescriptionSubmit,
projectId
}: H5MediaViewerProps) { }: H5MediaViewerProps) {
const carouselRef = useRef<CarouselRef>(null); const carouselRef = useRef<CarouselRef>(null);
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]); const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);

View File

@ -2,7 +2,7 @@
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react'; import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert /*, PenTool*/ } from 'lucide-react'; import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert, PenTool } from 'lucide-react';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal'; import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@ -12,9 +12,8 @@ import ScriptLoading from './script-loading';
import { TaskObject } from '@/api/DTO/movieEdit'; import { TaskObject } from '@/api/DTO/movieEdit';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
// 临时禁用视频编辑功能 import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
// import { VideoEditOverlay } from './video-edit/VideoEditOverlay'; import { EditPoint as EditPointType } from './video-edit/types';
// import { EditPoint as EditPointType } from './video-edit/types';
interface MediaViewerProps { interface MediaViewerProps {
taskObject: TaskObject; taskObject: TaskObject;
@ -35,9 +34,9 @@ interface MediaViewerProps {
onGotoCut: () => void; onGotoCut: () => void;
isSmartChatBoxOpen: boolean; isSmartChatBoxOpen: boolean;
onRetryVideo?: (video_id: string) => void; onRetryVideo?: (video_id: string) => void;
// 临时禁用视频编辑功能 enableVideoEdit?: boolean;
// enableVideoEdit?: boolean; onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
// onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void; projectId?: string;
} }
export const MediaViewer = React.memo(function MediaViewer({ export const MediaViewer = React.memo(function MediaViewer({
@ -58,10 +57,10 @@ export const MediaViewer = React.memo(function MediaViewer({
showGotoCutButton, showGotoCutButton,
onGotoCut, onGotoCut,
isSmartChatBoxOpen, isSmartChatBoxOpen,
onRetryVideo onRetryVideo,
// 临时禁用视频编辑功能 enableVideoEdit = true,
// enableVideoEdit = true, onVideoEditDescriptionSubmit,
// onVideoEditDescriptionSubmit projectId
}: MediaViewerProps) { }: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null); const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null); const finalVideoRef = useRef<HTMLVideoElement>(null);
@ -78,8 +77,7 @@ export const MediaViewer = React.memo(function MediaViewer({
const [toosBtnRight, setToodsBtnRight] = useState('1rem'); const [toosBtnRight, setToodsBtnRight] = useState('1rem');
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false); const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
// 临时禁用视频编辑功能 const [isVideoEditMode, setIsVideoEditMode] = useState(false);
// const [isVideoEditMode, setIsVideoEditMode] = useState(false);
useEffect(() => { useEffect(() => {
if (isSmartChatBoxOpen) { if (isSmartChatBoxOpen) {
@ -506,11 +504,10 @@ export const MediaViewer = React.memo(function MediaViewer({
}} }}
/> />
{/* 临时禁用视频编辑功能 */}
{/* 视频编辑覆盖层 */} {/* 视频编辑覆盖层 */}
{/*enableVideoEdit && isVideoEditMode && ( {enableVideoEdit && isVideoEditMode && (
<VideoEditOverlay <VideoEditOverlay
projectId={taskObject.project_id || ''} projectId={projectId || ''}
userId={JSON.parse(localStorage.getItem("currentUser") || '{}').id || 0} userId={JSON.parse(localStorage.getItem("currentUser") || '{}').id || 0}
currentVideo={{ currentVideo={{
id: taskObject.videos.data[currentSketchIndex].video_id, id: taskObject.videos.data[currentSketchIndex].video_id,
@ -522,16 +519,15 @@ export const MediaViewer = React.memo(function MediaViewer({
onDescriptionSubmit={onVideoEditDescriptionSubmit} onDescriptionSubmit={onVideoEditDescriptionSubmit}
className="rounded-lg" className="rounded-lg"
/> />
)*/} )}
</motion.div> </motion.div>
{/* 跳转剪辑按钮 */} {/* 跳转剪辑按钮 */}
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{ <div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight right: toosBtnRight
}}> }}>
{/* 临时禁用视频编辑功能 */} {/* 视频编辑模式切换按钮 - 临时注释 */}
{/* 视频编辑模式切换按钮 */} {/* {enableVideoEdit && (
{/*enableVideoEdit && (
<Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}> <Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}>
<GlassIconButton <GlassIconButton
icon={PenTool} icon={PenTool}
@ -540,7 +536,7 @@ export const MediaViewer = React.memo(function MediaViewer({
className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''} className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''}
/> />
</Tooltip> </Tooltip>
)*/} )} */}
{/* 添加到chat去编辑 按钮 */} {/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat"> <Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => { <GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {

View File

@ -69,7 +69,7 @@ function calculateCurvePath({
export function calculateInputPosition( export function calculateInputPosition(
editPointPosition: { x: number; y: number }, editPointPosition: { x: number; y: number },
containerSize: { width: number; height: number }, containerSize: { width: number; height: number },
inputBoxSize: { width: number; height: number } = { width: 200, height: 80 } inputBoxSize: { width: number; height: number } = { width: 300, height: 50 }
): InputBoxPosition { ): InputBoxPosition {
const { x: pointX, y: pointY } = editPointPosition; const { x: pointX, y: pointY } = editPointPosition;
const { width: containerWidth, height: containerHeight } = containerSize; const { width: containerWidth, height: containerHeight } = containerSize;
@ -151,26 +151,79 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
animated = true animated = true
}) => { }) => {
const { const {
color = 'rgba(255, 255, 255, 0.8)', color = 'rgba(255, 255, 255, 0.9)', // White color to match the reference image
strokeWidth = 2, strokeWidth = 2,
dashArray = '5,5' dashArray = '8,4' // Dashed line to match the reference image
} = style; } = style;
// 计算路径 // 计算箭头几何参数
const arrowSize = 8;
const arrowHalfHeight = 4;
// 计算连接方向和角度
const connectionVector = useMemo(() => {
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const length = Math.sqrt(dx * dx + dy * dy);
return {
dx: dx / length,
dy: dy / length,
angle: Math.atan2(dy, dx)
};
}, [startPoint, endPoint]);
// 计算箭头的正确位置和线条终点
const arrowGeometry = useMemo(() => {
const { dx, dy, angle } = connectionVector;
// 箭头尖端位置原endPoint
const arrowTip = { x: endPoint.x, y: endPoint.y };
// 箭头底部中心点(线条应该连接到这里)
const arrowBase = {
x: endPoint.x - dx * arrowSize,
y: endPoint.y - dy * arrowSize
};
// 计算箭头三角形的三个顶点
const perpX = -dy; // 垂直向量X
const perpY = dx; // 垂直向量Y
const arrowPoints = [
arrowTip, // 尖端
{
x: arrowBase.x + perpX * arrowHalfHeight,
y: arrowBase.y + perpY * arrowHalfHeight
},
{
x: arrowBase.x - perpX * arrowHalfHeight,
y: arrowBase.y - perpY * arrowHalfHeight
}
];
return {
tip: arrowTip,
base: arrowBase,
points: arrowPoints,
angle
};
}, [endPoint, connectionVector, arrowSize, arrowHalfHeight]);
// 计算路径(线条终止于箭头底部中心)
const path = useMemo(() => const path = useMemo(() =>
calculateCurvePath({ calculateCurvePath({
start: startPoint, start: startPoint,
end: endPoint, end: arrowGeometry.base, // 连接到箭头底部中心而不是尖端
containerSize, containerSize,
curvature curvature
}), [startPoint, endPoint, containerSize, curvature]); }), [startPoint, arrowGeometry.base, containerSize, curvature]);
// 计算路径长度用于动画 // 计算路径长度用于动画
const pathLength = useMemo(() => { const pathLength = useMemo(() => {
const dx = endPoint.x - startPoint.x; const dx = arrowGeometry.base.x - startPoint.x;
const dy = endPoint.y - startPoint.y; const dy = arrowGeometry.base.y - startPoint.y;
return Math.sqrt(dx * dx + dy * dy) * 1.2; // 弧线比直线长约20% return Math.sqrt(dx * dx + dy * dy) * 1.2; // 弧线比直线长约20%
}, [startPoint, endPoint]); }, [startPoint, arrowGeometry.base]);
return ( return (
<svg <svg
@ -179,7 +232,7 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
height={containerSize.height} height={containerSize.height}
style={{ zIndex: 10 }} style={{ zIndex: 10 }}
> >
{/* 连接线路径 */} {/* Curved dashed line - properly aligned to arrow base center */}
<motion.path <motion.path
d={path} d={path}
fill="none" fill="none"
@ -201,15 +254,13 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
opacity: { duration: 0.3 } opacity: { duration: 0.3 }
} : {}} } : {}}
style={{ style={{
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))' filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
}} }}
/> />
{/* 连接线末端的小圆点 */} {/* Properly aligned arrow head with geometric precision */}
<motion.circle <motion.polygon
cx={endPoint.x} points={arrowGeometry.points.map(p => `${p.x},${p.y}`).join(' ')}
cy={endPoint.y}
r={3}
fill={color} fill={color}
initial={animated ? { initial={animated ? {
scale: 0, scale: 0,
@ -227,32 +278,32 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
damping: 25 damping: 25
} : {}} } : {}}
style={{ style={{
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))' filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
}} }}
/> />
{/* 动画流动效果(可选) */} {/* Debug visualization (remove in production) */}
{animated && ( {process.env.NODE_ENV === 'development' && (
<motion.circle <>
r={2} {/* Arrow base center point */}
fill="rgba(255, 255, 255, 0.9)" <circle
initial={{ opacity: 0 }} cx={arrowGeometry.base.x}
animate={{ cy={arrowGeometry.base.y}
opacity: [0, 1, 0], r={1}
offsetDistance: ["0%", "100%"] fill="red"
}} opacity={0.5}
transition={{ />
duration: 2, {/* Arrow tip point */}
repeat: Infinity, <circle
ease: "linear", cx={arrowGeometry.tip.x}
delay: 0.6 cy={arrowGeometry.tip.y}
}} r={1}
style={{ fill="blue"
offsetPath: `path('${path}')`, opacity={0.5}
offsetRotate: "0deg" />
}} </>
/>
)} )}
</svg> </svg>
); );
}; };

View File

@ -101,7 +101,7 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
<AnimatePresence> <AnimatePresence>
{editPoint.description && editPoint.status !== EditPointStatus.PENDING && ( {editPoint.description && editPoint.status !== EditPointStatus.PENDING && (
<> <>
{/* 连接线 */} {/* White dashed connection line to match reference image */}
<motion.svg <motion.svg
className="absolute pointer-events-none" className="absolute pointer-events-none"
style={{ style={{
@ -118,30 +118,41 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
> >
<motion.path <motion.path
d={connectionPath} d={connectionPath}
stroke={statusColor} stroke="rgba(255, 255, 255, 0.9)"
strokeWidth={2} strokeWidth={2}
fill="none" fill="none"
strokeDasharray="4,4" strokeDasharray="8,4"
strokeLinecap="round"
initial={{ pathLength: 0, opacity: 0 }} initial={{ pathLength: 0, opacity: 0 }}
animate={{ animate={{
pathLength: 1, pathLength: 1,
opacity: 0.8, opacity: 1
strokeDashoffset: [0, -8]
}} }}
exit={{ pathLength: 0, opacity: 0 }} exit={{ pathLength: 0, opacity: 0 }}
transition={{ transition={{
pathLength: { duration: 0.8, ease: "easeOut" }, pathLength: { duration: 0.8, ease: "easeOut" },
opacity: { duration: 0.5 }, opacity: { duration: 0.5 }
strokeDashoffset: { }}
duration: 2, style={{
repeat: Infinity, filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
ease: "linear" }}
} />
{/* Arrow head */}
<motion.polygon
points={`${connectionEnd.x},${connectionEnd.y} ${connectionEnd.x-8},${connectionEnd.y-4} ${connectionEnd.x-8},${connectionEnd.y+4}`}
fill="rgba(255, 255, 255, 0.9)"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ delay: 0.4, duration: 0.3 }}
style={{
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
}} }}
/> />
</motion.svg> </motion.svg>
{/* 描述内容框 */} {/* Consistent white text display matching EditInput component */}
<motion.div <motion.div
className="absolute cursor-pointer group" className="absolute cursor-pointer group"
data-edit-description="true" data-edit-description="true"
@ -149,8 +160,6 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
left: position.x, left: position.x,
top: position.y, top: position.y,
zIndex: 25, zIndex: 25,
maxWidth: '300px',
minWidth: '200px'
}} }}
initial={{ initial={{
opacity: 0, opacity: 0,
@ -174,98 +183,67 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
duration: 0.4 duration: 0.4
}} }}
onClick={() => onClick?.(editPoint)} onClick={() => onClick?.(editPoint)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
> >
{/* 玻璃态背景 */} {/* White text display with exact same styling as EditInput */}
<div className="relative bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-lg shadow-lg border border-white/20 dark:border-gray-700/30 overflow-hidden"> <div className="flex items-center">
{/* 状态指示条 */}
<div <div
className="h-1 w-full" className="text-white font-bold text-lg tracking-wide uppercase"
style={{ backgroundColor: statusColor }} style={{
/> textShadow: '2px 2px 4px rgba(0, 0, 0, 0.8)',
fontFamily: 'Arial, sans-serif',
{/* 内容区域 */} letterSpacing: '0.1em'
<div className="p-3"> }}
{/* 状态标签 */} >
{statusText && ( {editPoint.description}
<div className="flex items-center gap-2 mb-2">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: statusColor }}
/>
<span
className="text-xs font-medium"
style={{ color: statusColor }}
>
{statusText}
</span>
</div>
)}
{/* 描述文本 */}
<div className="text-sm text-gray-800 dark:text-gray-200 leading-relaxed">
{editPoint.description}
</div>
{/* 时间戳 */}
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2 flex items-center justify-between">
<span>
{Math.floor(editPoint.timestamp)}s
</span>
<span>
{new Date(editPoint.updatedAt).toLocaleTimeString()}
</span>
</div>
</div> </div>
{/* 悬停时显示的操作按钮 */} {/* Interactive edit/delete buttons on hover */}
<motion.div <div className="ml-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-2">
className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100"
initial={{ opacity: 0 }}
animate={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
{onEdit && ( {onEdit && (
<button <motion.button
className="w-6 h-6 rounded-full bg-blue-500/80 hover:bg-blue-500 text-white text-xs flex items-center justify-center transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onEdit(editPoint.id); onEdit(editPoint.id);
}} }}
title="编辑" className="w-6 h-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="Edit description"
> >
</button> </motion.button>
)} )}
{onDelete && ( {onDelete && (
<button <motion.button
className="w-6 h-6 rounded-full bg-red-500/80 hover:bg-red-500 text-white text-xs flex items-center justify-center transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(editPoint.id); onDelete(editPoint.id);
}} }}
title="删除" className="w-6 h-6 rounded-full bg-red-500/20 hover:bg-red-500/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="Delete edit point"
> >
🗑 🗑
</button> </motion.button>
)} )}
</motion.div> </div>
{/* 装饰性光效 */}
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent pointer-events-none" />
</div> </div>
{/* 连接点指示器 */} {/* Status indicator for processing states */}
<div {editPoint.status === EditPointStatus.PROCESSING && (
className="absolute w-2 h-2 rounded-full border-2 border-white shadow-sm" <div className="mt-1 flex items-center">
style={{ <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse mr-2"></div>
backgroundColor: statusColor, <span className="text-white/70 text-xs uppercase tracking-wide">Processing...</span>
left: connectionEnd.x - position.x - 4, </div>
top: connectionEnd.y - position.y - 4, )}
}}
/> {editPoint.status === EditPointStatus.FAILED && (
<div className="mt-1 flex items-center">
<div className="w-2 h-2 bg-red-400 rounded-full mr-2"></div>
<span className="text-white/70 text-xs uppercase tracking-wide">Failed - Click to retry</span>
</div>
)}
</motion.div> </motion.div>
</> </>
)} )}

View File

@ -37,11 +37,10 @@ export const EditInput: React.FC<EditInputProps> = ({
isSubmitting = false, isSubmitting = false,
onSubmit, onSubmit,
onCancel, onCancel,
size = { width: 280, height: 120 }, size = { width: 300, height: 50 },
placeholder = "Describe your edit request..." placeholder = "Describe your edit..."
}) => { }) => {
const [description, setDescription] = useState(editPoint.description || ''); const [description, setDescription] = useState(editPoint.description || '');
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -116,7 +115,6 @@ export const EditInput: React.FC<EditInputProps> = ({
style={{ style={{
left: position.x, left: position.x,
top: position.y, top: position.y,
width: size.width,
}} }}
initial={{ initial={{
opacity: 0, opacity: 0,
@ -140,105 +138,33 @@ export const EditInput: React.FC<EditInputProps> = ({
duration: 0.3 duration: 0.3
}} }}
> >
{/* 玻璃态背景容器 */} {/* Input interface - focused on input functionality only */}
<div className="bg-black/60 backdrop-blur-md rounded-lg border border-white/20 shadow-2xl overflow-hidden"> <div className="flex items-center bg-white/90 backdrop-blur-sm rounded-lg px-3 py-2 shadow-lg">
{/* 头部 */} <input
<div className="px-3 py-2 border-b border-white/10 flex items-center justify-between"> ref={textareaRef as any}
<div className="flex items-center gap-2"> type="text"
<div className="w-2 h-2 rounded-full bg-blue-400"></div> value={description}
<span className="text-xs text-white/80 font-medium"> onChange={(e) => setDescription(e.target.value)}
Edit Request onKeyDown={handleKeyDown}
</span> placeholder={placeholder}
<span className="text-xs text-white/50"> disabled={isSubmitting}
{Math.floor(editPoint.timestamp)}s className="flex-1 bg-transparent text-gray-800 placeholder-gray-400 text-sm border-none outline-none min-w-[200px]"
</span> autoFocus
</div> />
<button
onClick={onCancel}
className="w-5 h-5 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white/70 hover:text-white transition-colors"
disabled={isSubmitting}
>
<X size={12} />
</button>
</div>
{/* 输入区域 */} {/* Submit button */}
<div className="p-3"> <button
<textarea onClick={handleSubmit}
ref={textareaRef} disabled={!description.trim() || isSubmitting}
value={description} className="ml-2 w-8 h-8 rounded-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white flex items-center justify-center flex-shrink-0"
onChange={(e) => setDescription(e.target.value)} >
onKeyDown={handleKeyDown} {isSubmitting ? (
onFocus={() => setIsFocused(true)} <div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
onBlur={() => setIsFocused(false)} ) : (
placeholder={placeholder} <span className="text-xs"></span>
disabled={isSubmitting}
className="w-full bg-transparent text-white placeholder-white/40 text-sm resize-none border-none outline-none min-h-[60px] max-h-[120px]"
style={{ lineHeight: '1.4' }}
/>
</div>
{/* 底部操作区 */}
<div className="px-3 py-2 border-t border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-white/50">
<span>Ctrl+Enter to submit</span>
<span></span>
<span>Esc to cancel</span>
</div>
<div className="flex items-center gap-2">
{/* 字符计数 */}
<span className={`text-xs ${
description.length > 500 ? 'text-red-400' : 'text-white/50'
}`}>
{description.length}/500
</span>
{/* 提交按钮 */}
<motion.button
onClick={handleSubmit}
disabled={!description.trim() || isSubmitting || description.length > 500}
className="flex items-center gap-1 px-3 py-1.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-500 disabled:cursor-not-allowed text-white text-xs rounded-md transition-colors"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{isSubmitting ? (
<>
<Loader2 size={12} className="animate-spin" />
<span>Submitting...</span>
</>
) : (
<>
<Send size={12} />
<span>Submit</span>
</>
)}
</motion.button>
</div>
</div>
{/* 聚焦时的发光边框 */}
<AnimatePresence>
{isFocused && (
<motion.div
className="absolute inset-0 rounded-lg border-2 border-blue-400/50 pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)} )}
</AnimatePresence> </button>
</div> </div>
{/* 输入框指示箭头(可选) */}
<div
className="absolute w-3 h-3 bg-black/60 border-l border-t border-white/20 transform rotate-45"
style={{
left: 12,
top: -6,
}}
/>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -71,44 +71,7 @@ export const EditPoint: React.FC<EditPointProps> = ({
onEdit(editPoint.id); onEdit(editPoint.id);
}, [onEdit, editPoint.id]); }, [onEdit, editPoint.id]);
// 根据状态获取颜色 // Simplified for the image design - just use blue color
const getStatusColor = useCallback(() => {
switch (editPoint.status) {
case EditPointStatus.PENDING:
return '#f59e0b'; // 黄色
case EditPointStatus.EDITED:
return '#10b981'; // 绿色
case EditPointStatus.PROCESSING:
return '#3b82f6'; // 蓝色
case EditPointStatus.COMPLETED:
return '#059669'; // 深绿色
case EditPointStatus.FAILED:
return '#ef4444'; // 红色
default:
return color;
}
}, [editPoint.status, color]);
// 根据状态获取图标
const getStatusIcon = useCallback(() => {
switch (editPoint.status) {
case EditPointStatus.PENDING:
return Edit3;
case EditPointStatus.EDITED:
return Check;
case EditPointStatus.PROCESSING:
return Loader2;
case EditPointStatus.COMPLETED:
return Check;
case EditPointStatus.FAILED:
return X;
default:
return Edit3;
}
}, [editPoint.status]);
const StatusIcon = getStatusIcon();
const statusColor = getStatusColor();
return ( return (
<motion.div <motion.div
@ -119,7 +82,7 @@ export const EditPoint: React.FC<EditPointProps> = ({
top: absolutePosition.y - size / 2, top: absolutePosition.y - size / 2,
}} }}
initial={{ scale: 0, opacity: 0 }} initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 0 }} // Make invisible to match reference image
exit={{ scale: 0, opacity: 0 }} exit={{ scale: 0, opacity: 0 }}
transition={{ transition={{
type: "spring", type: "spring",
@ -129,136 +92,17 @@ export const EditPoint: React.FC<EditPointProps> = ({
}} }}
onClick={handleClick} onClick={handleClick}
> >
{/* 脉冲动画背景 */} {/* Invisible edit point - just for click handling */}
<AnimatePresence>
{(editPoint.status === EditPointStatus.PENDING || isSelected) && (
<motion.div
className="absolute inset-0 rounded-full"
style={{
backgroundColor: pulseColor,
width: size * 3,
height: size * 3,
left: -size,
top: -size,
}}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: [1, 1.5, 1],
opacity: [0.6, 0.2, 0.6],
}}
exit={{ scale: 0, opacity: 0 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
)}
</AnimatePresence>
{/* 主编辑点 */}
<motion.div <motion.div
className="relative rounded-full flex items-center justify-center shadow-lg backdrop-blur-sm" className="relative rounded-full"
style={{ style={{
width: size, width: size * 2, // Larger click area
height: size, height: size * 2,
backgroundColor: statusColor, backgroundColor: 'transparent', // Invisible
border: `2px solid rgba(255, 255, 255, 0.3)`,
}} }}
whileHover={{ scale: 1.2 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
animate={editPoint.status === EditPointStatus.PROCESSING ? { />
rotate: 360,
transition: { duration: 1, repeat: Infinity, ease: "linear" }
} : {}}
>
<StatusIcon
size={size * 0.5}
color="white"
className={editPoint.status === EditPointStatus.PROCESSING ? "animate-spin" : ""}
/>
</motion.div>
{/* 选中状态的操作按钮 */}
<AnimatePresence>
{isSelected && editPoint.status !== EditPointStatus.PROCESSING && (
<motion.div
className="absolute flex gap-1"
style={{
left: size + 8,
top: -4,
}}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.2 }}
>
{/* 编辑按钮 */}
<motion.button
className="w-6 h-6 rounded-full bg-blue-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-blue-600/80 transition-colors"
onClick={handleEdit}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="编辑描述"
>
<Edit3 size={12} />
</motion.button>
{/* 删除按钮 */}
<motion.button
className="w-6 h-6 rounded-full bg-red-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-red-600/80 transition-colors"
onClick={handleDelete}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title="删除编辑点"
>
<X size={12} />
</motion.button>
</motion.div>
)}
</AnimatePresence>
{/* 状态提示文本 */}
<AnimatePresence>
{isSelected && editPoint.description && (
<motion.div
className="absolute whitespace-nowrap text-xs text-white bg-black/60 backdrop-blur-sm rounded px-2 py-1 pointer-events-none"
style={{
left: size + 8,
top: size + 8,
maxWidth: 200,
}}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{editPoint.description.length > 50
? `${editPoint.description.substring(0, 50)}...`
: editPoint.description
}
</motion.div>
)}
</AnimatePresence>
{/* 时间戳显示 */}
<AnimatePresence>
{isSelected && (
<motion.div
className="absolute text-xs text-white/70 bg-black/40 backdrop-blur-sm rounded px-1 py-0.5 pointer-events-none"
style={{
left: -20,
top: size + 8,
}}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
>
{Math.floor(editPoint.timestamp)}s
</motion.div>
)}
</AnimatePresence>
</motion.div> </motion.div>
); );
}; };

View File

@ -275,7 +275,8 @@ export const VideoEditOverlay: React.FC<VideoEditOverlayProps> = ({
<AnimatePresence> <AnimatePresence>
{editPoints.map(editPoint => { {editPoints.map(editPoint => {
const elementPosition = elementPositions[editPoint.id]; const elementPosition = elementPositions[editPoint.id];
// 只显示已提交且有描述的编辑点
// 只显示已提交且有描述的编辑点,且不在输入模式
if ( if (
!editPoint.description || !editPoint.description ||
editPoint.description.trim() === '' || editPoint.description.trim() === '' ||

View File

@ -18,20 +18,20 @@ import { createEditPoint, updateEditPoint, deleteEditPoint, getEditPoints } from
import { debounce } from './utils'; import { debounce } from './utils';
/** /**
* * -
*/ */
const DEFAULT_CONFIG: VideoEditConfig = { const DEFAULT_CONFIG: VideoEditConfig = {
enabled: true, enabled: true,
maxEditPoints: 10, maxEditPoints: 10,
connectionStyle: { connectionStyle: {
color: 'rgba(255, 255, 255, 0.8)', color: 'rgba(255, 255, 255, 0.9)', // 白色连接线匹配参考图像
strokeWidth: 2, strokeWidth: 2,
dashArray: '5,5' dashArray: '8,4' // 虚线样式匹配参考图像
}, },
pointStyle: { pointStyle: {
size: 12, size: 12,
color: '#3b82f6', color: 'transparent', // 透明编辑点匹配参考图像
pulseColor: 'rgba(59, 130, 246, 0.3)' pulseColor: 'transparent' // 透明脉冲动画
} }
}; };