video-flow-b/components/ui/replace-character-modal.tsx
2025-06-29 00:04:15 +08:00

266 lines
10 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Upload, Library, Wand2, Search, Image, Plus, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ReplaceCharacterModalProps {
isOpen: boolean;
activeReplaceMethod: string;
onClose: () => void;
onCharacterSelect: (character: any) => void;
}
export function ReplaceCharacterModal({
isOpen,
activeReplaceMethod,
onClose,
onCharacterSelect
}: ReplaceCharacterModalProps) {
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
setActiveMethod(activeReplaceMethod);
}, [activeReplaceMethod]);
// 模拟角色库数据
const mockLibraryCharacters = [
{
id: 1,
avatar: '/assets/3dr_chihiro.png',
name: '雪 (YUKI)',
style: '动漫风格'
},
{
id: 2,
avatar: '/assets/3dr_mono.png',
name: '春 (HARU)',
style: '写实风格'
},
{
id: 3,
avatar: '/assets/3dr_chihiro.png',
name: '夏 (NATSU)',
style: '二次元风格'
},
];
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// 处理文件上传
console.log('Uploading file:', file);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-2">
<button
className="p-1 hover:bg-white/10 rounded-full transition-colors"
onClick={onClose}
>
<ChevronDown className="w-5 h-5" />
</button>
<h2 className="text-lg font-medium"></h2>
</div>
</div>
{/* 主要内容区域 */}
<div className="p-6 space-y-6 h-[80vh]">
{/* 替换方式选择 */}
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'upload'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('upload')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'library'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('library')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
</div>
{/* 内容区域 */}
<AnimatePresence mode="wait">
{activeMethod === 'upload' && (
<motion.div
key="upload"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
>
<input
type="file"
accept="image/*"
className="hidden"
id="character-upload"
onChange={handleFileUpload}
/>
<label
htmlFor="character-upload"
className="flex flex-col items-center gap-4 cursor-pointer"
>
<motion.div
className="p-4 rounded-full bg-white/5"
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
whileTap={{ scale: 0.9 }}
>
<Image className="w-8 h-8 text-white/70" />
</motion.div>
<div className="text-center">
<p className="text-white/70"></p>
<p className="text-sm text-white/50 mt-2"> PNG, JPG, WEBP </p>
</div>
</label>
</motion.div>
)}
{activeMethod === 'library' && (
<motion.div
key="library"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-4"
>
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
<input
type="text"
placeholder="搜索角色..."
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* 角色列表 */}
<div className="grid grid-cols-4 gap-4">
{mockLibraryCharacters.map((character) => (
<motion.div
key={character.id}
className="group relative aspect-[9/16] rounded-lg overflow-hidden cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onCharacterSelect(character)}
>
<img
src={character.avatar}
alt={character.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent
opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-2">
<p className="text-sm font-medium">{character.name}</p>
<p className="text-xs text-white/70">{character.style}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
{activeMethod === 'generate' && (
<motion.div
key="generate"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center gap-4"
>
<motion.button
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
rounded-lg transition-colors"
onClick={() => console.log('Generate character')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Plus className="w-5 h-5" />
<span></span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
);
}