video-flow-b/components/ui/replace-panel.tsx
2025-08-01 13:30:50 +08:00

195 lines
6.0 KiB
TypeScript
Raw 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 } from 'react';
import { motion } from 'framer-motion';
import { Check, X, CircleAlert } from 'lucide-react';
import { cn } from '@/public/lib/utils';
// 定义类型
interface Shot {
id: string;
videoUrl?: string;
thumbnailUrl: string;
isGenerating: boolean;
isSelected: boolean;
}
interface Item {
id: string;
name: string;
avatarUrl: string;
}
interface ReplacePanelProps {
title: string;
shots: Shot[];
item: Item;
showAddToLibrary?: boolean;
addToLibraryText?: string;
onClose: () => void;
onConfirm: (selectedShots: string[], addToLibrary: boolean) => void;
}
export function ReplacePanel({
title,
shots,
item,
showAddToLibrary = false,
addToLibraryText = "同步添加至库",
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 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);
};
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-sm text-red-400">
<CircleAlert className="w-4 h-4" />
<span className="text-blue-500">{shots.length}</span>
</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">
<div className="text-white/80 text-sm"></div>
<div className="flex gap-4 overflow-x-auto pb-4 hide-scrollbar h-64">
{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 && (
<video
ref={el => {
if (el) videoRefs.current[shot.id] = el;
}}
src={shot.videoUrl}
className="w-full h-full object-cover"
loop
muted
playsInline
/>
)}
{!shot.videoUrl && (
<img
src={shot.thumbnailUrl}
alt={`Shot ${shot.id}`}
className="w-full h-full object-cover"
/>
)}
{shot.isGenerating && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="text-white text-sm">...</div>
</div>
)}
{selectedShots.includes(shot.id) && (
<div className="absolute top-2 right-2">
<Check className="w-4 h-4 text-blue-500" />
</div>
)}
</motion.div>
))}
</div>
</div>
{/* 预览信息 */}
<div className="flex items-center gap-4 bg-white/5 rounded-lg p-4">
<img
src={item.avatarUrl}
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"
>
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
>
</button>
</div>
</div>
);
}