video-flow-b/components/ui/replace-panel.tsx
2025-08-18 20:20:31 +08:00

244 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Check, X, CircleAlert, ArrowLeft, ArrowRight, Loader2 } from 'lucide-react';
import { cn } from '@/public/lib/utils';
import { throttle } from 'lodash';
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 videoRefs = useRef<{ [key: string]: HTMLVideoElement }>({});
const shotsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
console.log('replace-panel-shots', shots);
}, [shots]);
const handleShotToggle = (shotId: string) => {
// setSelectedShots(prev =>
// prev.includes(shotId)
// ? prev.filter(id => id !== shotId)
// : [...prev, shotId]
// );
};
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 throttledConfirm = React.useCallback(
throttle(() => {
onConfirm(selectedShots, addToLibrary);
}, 1000, { trailing: false }), // 1秒内只能触发一次不要执行最后一次调用
[selectedShots, addToLibrary, onConfirm]
);
// 在组件卸载时取消节流函数中的定时器
React.useEffect(() => {
return () => {
throttledConfirm.cancel();
};
}, [throttledConfirm]);
const handleConfirm = () => {
throttledConfirm();
};
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.status === 0 || !shot.videoUrl.length) && (
<div className="w-full h-full absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="text-white text-sm">
<Loader2 className="w-4 h-4 animate-spin" />
</div>
</div>
)}
{shot.status === 2 && (
<div className="w-full h-full absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="text-white text-sm">
<CircleAlert className="w-4 h-4" />
</div>
</div>
)}
{shot.status === 1 && shot.videoUrl.length && (
<video
ref={el => {
if (el) videoRefs.current[shot.id] = el;
}}
src={shot.videoUrl[0]}
className="w-full h-full object-cover"
loop
muted
playsInline
/>
)}
</>
</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",
"opacity-100 cursor-pointer hover:bg-black/70"
)}
onClick={() => 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",
"opacity-100 cursor-pointer hover:bg-black/70"
)}
onClick={() => 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>
);
}