video-flow-b/components/ui/replace-panel.tsx
2025-08-16 13:31:45 +08:00

249 lines
8.1 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { motion } from 'framer-motion';
import { Check, X, CircleAlert, ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/public/lib/utils';
interface ReplacePanelProps {
isLoading: boolean;
title: string;
shots: any[];
item: any;
showAddToLibrary?: boolean;
addToLibraryText?: string;
onClose: () => void;
onConfirm: (selectedShots: string[], addToLibrary: boolean) => void;
}
export function ReplacePanel({
isLoading,
title,
shots,
item,
showAddToLibrary = false,
addToLibraryText = "Add to library",
onClose,
onConfirm,
}: ReplacePanelProps) {
const [selectedShots, setSelectedShots] = useState(
shots.filter(shot => shot.isSelected).map(shot => shot.id)
);
const [addToLibrary, setAddToLibrary] = useState(false);
const [hoveredVideoId, setHoveredVideoId] = useState<string | null>(null);
const [isAtStart, setIsAtStart] = useState(true);
const [isAtEnd, setIsAtEnd] = useState(false);
const videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({});
const shotsRef = useRef<HTMLDivElement>(null);
// 检查滚动位置
const checkScrollPosition = () => {
if (!shotsRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } = shotsRef.current;
setIsAtStart(scrollLeft <= 0);
setIsAtEnd(Math.ceil(scrollLeft + clientWidth) >= scrollWidth);
};
// 添加滚动事件监听
React.useEffect(() => {
const shotsElement = shotsRef.current;
if (!shotsElement) return;
shotsElement.addEventListener('scroll', checkScrollPosition);
// 初始检查
checkScrollPosition();
return () => {
shotsElement.removeEventListener('scroll', checkScrollPosition);
};
}, []);
const handleShotToggle = (shotId: string) => {
// setSelectedShots(prev =>
// prev.includes(shotId)
// ? prev.filter(id => id !== shotId)
// : [...prev, shotId]
// );
};
const handleSelectAllShots = (checked: boolean) => {
setSelectedShots(checked ? shots.map(shot => shot.id) : []);
};
const handleMouseEnter = (shotId: string) => {
setHoveredVideoId(shotId);
if (videoRefs.current[shotId]) {
videoRefs.current[shotId].play();
}
};
const handleMouseLeave = (shotId: string) => {
setHoveredVideoId(null);
if (videoRefs.current[shotId]) {
videoRefs.current[shotId].pause();
videoRefs.current[shotId].currentTime = 0;
}
};
const handleConfirm = () => {
onConfirm(selectedShots, addToLibrary);
};
const handleLeftArrowClick = () => {
if (!shotsRef.current) return;
shotsRef.current.scrollBy({
left: -300, // 每次滚动的距离
behavior: 'smooth' // 平滑滚动
});
};
const handleRightArrowClick = () => {
if (!shotsRef.current) return;
shotsRef.current.scrollBy({
left: 300, // 每次滚动的距离
behavior: 'smooth' // 平滑滚动
});
};
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center w-full max-w-5xl text-white/50">
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
<p>Loading...</p>
</div>
)
}
return (
<div className="space-y-2 w-full max-w-5xl">
{/* 标题 */}
<div className="text-2xl font-semibold text-white">{title}</div>
{/* 提示信息 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-red-400">
<CircleAlert className="w-4 h-4" />
This role appears in <span className="text-blue-500 font-bold">{shots.length}</span> shots, replacing it will affect the following shots.
</div>
{/* <div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedShots.length === shots.length}
onChange={(e) => handleSelectAllShots(e.target.checked)}
className="w-4 h-4 rounded border-white/20"
/>
<label className="text-white/80">全选</label>
</div> */}
</div>
{/* 分镜展示区 */}
<div className="space-y-2 relative">
{/* <div className="text-white/80 text-sm">选择需要替换的分镜:</div> */}
<div className="relative flex gap-4 overflow-x-auto pb-4 hide-scrollbar h-64" ref={shotsRef}>
{shots.map((shot) => (
<motion.div
key={shot.id}
className={cn(
'relative flex-shrink-0 rounded-lg overflow-hidden cursor-pointer',
'aspect-video border-2',
hoveredVideoId === shot.id ? 'w-auto' : 'w-32',
selectedShots.includes(shot.id)
? 'border-blue-500'
: 'border-transparent hover:border-blue-500/50'
)}
onClick={() => handleShotToggle(shot.id)}
onMouseEnter={() => handleMouseEnter(shot.id)}
onMouseLeave={() => handleMouseLeave(shot.id)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{shot.videoUrl && shot.videoUrl.length > 0 && (
<video
ref={el => {
if (el) videoRefs.current[shot.id] = el;
}}
src={shot.videoUrl[0].video_url}
className="w-full h-full object-cover"
loop
muted
playsInline
/>
)}
{(!shot.videoUrl || shot.videoUrl.length === 0) && (
<>
<img
src={shot.sketchUrl}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="text-white text-sm">Generating...</div>
</div>
</>
)}
</motion.div>
))}
</div>
{/* 左右箭头 */}
<div
className={cn(
"absolute top-1/2 -translate-y-1/2 left-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
isAtStart ? "opacity-30 cursor-not-allowed" : "opacity-100 cursor-pointer hover:bg-black/70"
)}
onClick={() => !isAtStart && handleLeftArrowClick()}
>
<ArrowLeft className="w-4 h-4 text-white" />
</div>
<div
className={cn(
"absolute top-1/2 -translate-y-1/2 right-0 w-10 h-10 rounded-full bg-black/50 flex items-center justify-center transition-opacity duration-200",
isAtEnd ? "opacity-30 cursor-not-allowed" : "opacity-100 cursor-pointer hover:bg-black/70"
)}
onClick={() => !isAtEnd && handleRightArrowClick()}
>
<ArrowRight className="w-4 h-4 text-white" />
</div>
</div>
{/* 预览信息 */}
<div className="flex items-center gap-4 bg-white/5 rounded-lg p-4">
<img
src={item.imageUrl}
alt={item.name}
className="w-12 h-12 rounded-full object-cover"
/>
<div className="text-white">{item.name}</div>
</div>
{/* 同步到库选项 */}
{showAddToLibrary && (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={addToLibrary}
onChange={(e) => setAddToLibrary(e.target.checked)}
className="w-4 h-4 rounded border-white/20"
/>
<label className="text-white/80">{addToLibraryText}</label>
</div>
)}
{/* 操作按钮 */}
<div className="flex justify-end gap-4">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
>
Cancel
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
>
Replace
</button>
</div>
</div>
);
}