Initial commit: CarrotSkin project setup
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon} from '@heroicons/react/24/outline';
|
||||
import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion';
|
||||
import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon } from '@heroicons/react/24/outline';
|
||||
import SkinViewer from './SkinViewer';
|
||||
|
||||
interface SkinDetailModalProps {
|
||||
@@ -22,13 +22,21 @@ interface SkinDetailModalProps {
|
||||
username: string;
|
||||
};
|
||||
} | null;
|
||||
isExternalPreview?: boolean;
|
||||
}
|
||||
|
||||
export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetailModalProps) {
|
||||
export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPreview = false }: SkinDetailModalProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'jumping'>('idle');
|
||||
const [autoRotate, setAutoRotate] = useState(true);
|
||||
const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'swimming'>('idle');
|
||||
const [autoRotate, setAutoRotate] = useState(!isExternalPreview);
|
||||
const [rotation, setRotation] = useState(true);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'preview' | 'info' | 'settings'>('preview');
|
||||
|
||||
// 弹簧动画配置
|
||||
const springConfig = { stiffness: 300, damping: 30 };
|
||||
const scale = useSpring(1, springConfig);
|
||||
const rotate = useSpring(0, springConfig);
|
||||
|
||||
// 重置状态当对话框关闭时
|
||||
useEffect(() => {
|
||||
@@ -37,6 +45,8 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
|
||||
setCurrentAnimation('idle');
|
||||
setAutoRotate(true);
|
||||
setRotation(true);
|
||||
setIsMinimized(false);
|
||||
setActiveTab('preview');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -63,14 +73,101 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
|
||||
setCurrentAnimation('running');
|
||||
break;
|
||||
case '4':
|
||||
setCurrentAnimation('jumping');
|
||||
setCurrentAnimation('swimming');
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
setIsMinimized(!isMinimized);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose, isPlaying]);
|
||||
}, [isOpen, onClose, isPlaying, isMinimized]);
|
||||
|
||||
// 动画控制函数
|
||||
const handleAnimationChange = (animation: 'idle' | 'walking' | 'running' | 'swimming') => {
|
||||
setCurrentAnimation(animation);
|
||||
// 添加触觉反馈
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
};
|
||||
|
||||
const getModalVariants = () => ({
|
||||
initial: {
|
||||
opacity: 0,
|
||||
scale: 0.7,
|
||||
rotateX: -15,
|
||||
y: 50
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotateX: 0,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
scale: 0.7,
|
||||
rotateX: 15,
|
||||
y: 50,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeIn"
|
||||
}
|
||||
},
|
||||
minimized: {
|
||||
scale: 0.8,
|
||||
y: window.innerHeight - 200,
|
||||
x: window.innerWidth - 300,
|
||||
width: 280,
|
||||
height: 150,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: "easeInOut"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getAnimationButtonVariants = (isActive: boolean) => ({
|
||||
initial: { scale: 0.9, opacity: 0.8 },
|
||||
animate: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 20
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
scale: 1.05,
|
||||
transition: { duration: 0.2 }
|
||||
},
|
||||
tap: {
|
||||
scale: 0.95,
|
||||
transition: { duration: 0.1 }
|
||||
},
|
||||
active: {
|
||||
scale: 1.02,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 25
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!texture) return null;
|
||||
|
||||
@@ -81,235 +178,306 @@ export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetail
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
isolation: 'isolate'
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
variants={getModalVariants()}
|
||||
initial="initial"
|
||||
animate={isMinimized ? "minimized" : "animate"}
|
||||
exit="exit"
|
||||
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||
className="relative w-full max-w-6xl h-[90vh] bg-white/95 dark:bg-gray-800/95 rounded-2xl shadow-2xl overflow-hidden"
|
||||
className={`relative bg-white/95 dark:bg-gray-800/95 rounded-2xl shadow-2xl overflow-hidden border border-white/20 dark:border-gray-700/50 backdrop-blur-lg ${
|
||||
isMinimized ? 'fixed bottom-4 right-4' : 'w-full max-w-6xl h-[90vh]'
|
||||
}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="absolute top-0 left-0 right-0 z-10 bg-gradient-to-r from-white/90 to-gray-50/90 dark:from-gray-800/90 dark:to-gray-700/90 backdrop-blur-md border-b border-gray-200/50 dark:border-gray-600/50 p-4">
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 right-0 bg-gradient-to-r from-white/90 to-orange-50/90 dark:from-gray-800/90 dark:to-gray-700/90 backdrop-blur-md border-b border-orange-200/50 dark:border-gray-600/50 p-4 z-10"
|
||||
initial={{ y: -100 }}
|
||||
animate={{ y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
<motion.h2
|
||||
className="text-2xl font-bold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
{texture.name}
|
||||
</h2>
|
||||
</motion.h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`px-3 py-1 text-sm rounded-full font-medium ${
|
||||
texture.type === 'SKIN'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
}`}>
|
||||
<motion.span
|
||||
className={`px-3 py-1 text-sm rounded-full font-medium ${
|
||||
texture.type === 'SKIN'
|
||||
? 'bg-gradient-to-r from-orange-100 to-amber-100 text-orange-800 dark:from-orange-900/30 dark:to-amber-900/30 dark:text-orange-300'
|
||||
: 'bg-gradient-to-r from-purple-100 to-pink-100 text-purple-800 dark:from-purple-900/30 dark:to-pink-900/30 dark:text-purple-300'
|
||||
}`}
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, type: "spring", stiffness: 200 }}
|
||||
>
|
||||
{texture.type === 'SKIN' ? '皮肤' : '披风'}
|
||||
</span>
|
||||
</motion.span>
|
||||
{texture.is_slim && (
|
||||
<span className="px-3 py-1 bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300 text-sm rounded-full font-medium">
|
||||
<motion.span
|
||||
className="px-3 py-1 bg-gradient-to-r from-pink-100 to-rose-100 text-pink-800 dark:from-pink-900/30 dark:to-rose-900/30 text-sm rounded-full font-medium"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.5, type: "spring", stiffness: 200 }}
|
||||
>
|
||||
细臂
|
||||
</span>
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
<div className="flex items-center space-x-2">
|
||||
<motion.button
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
className="p-2 text-gray-500 hover:text-orange-500 dark:text-gray-400 dark:hover:text-orange-400 rounded-full hover:bg-orange-100 dark:hover:bg-orange-900/20 transition-all duration-200"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
initial={{ opacity: 0, rotate: -180 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
title={isMinimized ? "最大化" : "最小化"}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isMinimized ? "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" : "M9 9V4.5M9 9H4.5M9 9L3.5 3.5M15 9v4.5M15 9h4.5M15 9l5.5 5.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 15v4.5M15 15h4.5m-4.5 0l5.5 5.5"} />
|
||||
</svg>
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-500 hover:text-orange-500 dark:text-gray-400 dark:hover:text-orange-400 rounded-full hover:bg-orange-100 dark:hover:bg-orange-900/20 transition-all duration-200"
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
initial={{ opacity: 0, rotate: -180 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{!isMinimized && (
|
||||
<div className="flex h-full pt-20">
|
||||
{/* 3D 预览区域 */}
|
||||
<motion.div
|
||||
className="flex-1 flex items-center justify-center p-8 bg-gradient-to-br from-orange-50/30 to-amber-50/30 dark:from-gray-900/30 dark:to-gray-800/30"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full pt-20">
|
||||
{/* 3D 预览区域 */}
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="w-full h-full max-w-2xl max-h-2xl">
|
||||
<SkinViewer
|
||||
skinUrl={texture.url}
|
||||
isSlim={texture.is_slim}
|
||||
width={600}
|
||||
height={600}
|
||||
className="w-full h-full rounded-xl shadow-lg"
|
||||
autoRotate={autoRotate}
|
||||
walking={currentAnimation === 'walking'}
|
||||
running={currentAnimation === 'running'}
|
||||
jumping={currentAnimation === 'jumping'}
|
||||
rotation={rotation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 控制面板 */}
|
||||
<div className="w-80 bg-gray-50/80 dark:bg-gray-900/80 backdrop-blur-md border-l border-gray-200/50 dark:border-gray-600/50 p-6 space-y-6 overflow-y-auto">
|
||||
{/* 动画控制 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<PlayIcon className="w-5 h-5 mr-2" />
|
||||
动画控制
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => setCurrentAnimation('idle')}
|
||||
className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
currentAnimation === 'idle'
|
||||
? 'bg-orange-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
<div className="w-full h-full max-w-2xl max-h-2xl">
|
||||
<motion.div
|
||||
animate={{
|
||||
rotateY: autoRotate ? 360 : 0,
|
||||
scale: isPlaying ? 1.02 : 1
|
||||
}}
|
||||
transition={{
|
||||
rotateY: { duration: 10, repeat: Infinity, ease: "linear" },
|
||||
scale: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
静止
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => setCurrentAnimation('walking')}
|
||||
className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
currentAnimation === 'walking'
|
||||
? 'bg-orange-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
步行
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => setCurrentAnimation('running')}
|
||||
className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
currentAnimation === 'running'
|
||||
? 'bg-orange-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
跑步
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
onClick={() => setCurrentAnimation('jumping')}
|
||||
className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
currentAnimation === 'jumping'
|
||||
? 'bg-orange-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
跳跃
|
||||
</motion.button>
|
||||
<SkinViewer
|
||||
skinUrl={texture.url}
|
||||
isSlim={texture.is_slim}
|
||||
width={600}
|
||||
height={600}
|
||||
className="w-full h-full rounded-xl shadow-2xl border-2 border-white/50 dark:border-gray-700/50"
|
||||
autoRotate={autoRotate}
|
||||
walking={currentAnimation === 'walking'}
|
||||
running={currentAnimation === 'running'}
|
||||
jumping={currentAnimation === 'swimming'}
|
||||
rotation={rotation}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 视角控制 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<ArrowPathIcon className="w-5 h-5 mr-2" />
|
||||
视角控制
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<motion.button
|
||||
onClick={() => setAutoRotate(!autoRotate)}
|
||||
className={`w-full p-3 rounded-lg text-sm font-medium transition-all duration-200 flex items-center justify-center ${
|
||||
autoRotate
|
||||
? 'bg-blue-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
{/* 控制面板 */}
|
||||
<motion.div
|
||||
className="w-80 bg-gradient-to-b from-orange-50/80 to-amber-50/80 dark:from-gray-900/80 dark:to-gray-800/80 backdrop-blur-md border-l border-orange-200/50 dark:border-gray-600/50 p-6 space-y-6 overflow-y-auto custom-scrollbar"
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.5 }}
|
||||
>
|
||||
{/* 动画控制 */}
|
||||
<motion.div className="space-y-4">
|
||||
<motion.h3
|
||||
className="text-lg font-semibold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent flex items-center"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<ArrowPathIcon className="w-4 h-4 mr-2" />
|
||||
{autoRotate ? '停止旋转' : '自动旋转'}
|
||||
</motion.button>
|
||||
<PlayIcon className="w-5 h-5 mr-2" />
|
||||
动画控制
|
||||
</motion.h3>
|
||||
|
||||
<motion.button
|
||||
onClick={() => setRotation(!rotation)}
|
||||
className={`w-full p-3 rounded-lg text-sm font-medium transition-all duration-200 flex items-center justify-center ${
|
||||
rotation
|
||||
? 'bg-green-500 text-white shadow-lg'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<ForwardIcon className="w-4 h-4 mr-2" />
|
||||
{rotation ? '禁用控制' : '启用控制'}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 皮肤信息 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<span className="w-5 h-5 mr-2">📋</span>
|
||||
皮肤信息
|
||||
</h3>
|
||||
|
||||
<div className="bg-white/50 dark:bg-gray-800/50 rounded-lg p-4 space-y-3">
|
||||
{texture.description && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">描述</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white">{texture.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-600 dark:text-gray-400">收藏数</p>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{texture.favorite_count || 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-600 dark:text-gray-400">下载数</p>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{texture.download_count || 0}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ key: 'idle', label: '静止', icon: null },
|
||||
{ key: 'walking', label: '步行', icon: null },
|
||||
{ key: 'running', label: '跑步', icon: null },
|
||||
{ key: 'swimming', label: '游泳', icon: null }
|
||||
].map((anim, i) => (
|
||||
<motion.button
|
||||
key={anim.key}
|
||||
variants={getAnimationButtonVariants(currentAnimation === anim.key)}
|
||||
initial="initial"
|
||||
animate={currentAnimation === anim.key ? "active" : "animate"}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onClick={() => handleAnimationChange(anim.key as any)}
|
||||
className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
currentAnimation === anim.key
|
||||
? 'bg-gradient-to-r from-orange-500 to-amber-500 text-white shadow-lg transform scale-105'
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-orange-100 dark:hover:bg-orange-900/20 border border-orange-200 dark:border-gray-600 hover:border-orange-300 dark:hover:border-orange-500'
|
||||
}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 + i * 0.1 }}
|
||||
>
|
||||
{anim.label}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{texture.uploader && (
|
||||
<div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">上传者</p>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{texture.uploader.username}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{texture.created_at && (
|
||||
<div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">上传时间</p>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
{new Date(texture.created_at).toLocaleDateString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 快捷键提示 */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-blue-800 dark:text-blue-300 mb-2 flex items-center">
|
||||
<span className="w-4 h-4 mr-2">⌨️</span>
|
||||
快捷键
|
||||
</h4>
|
||||
<div className="text-xs text-blue-700 dark:text-blue-400 space-y-1">
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">空格</kbd> 播放/暂停</p>
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">1</kbd> 静止</p>
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">2</kbd> 步行</p>
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">3</kbd> 跑步</p>
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">4</kbd> 跳跃</p>
|
||||
<p><kbd className="px-1 py-0.5 bg-white/50 dark:bg-gray-800/50 rounded">ESC</kbd> 关闭</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* 信息面板 */}
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
<h3 className="text-lg font-semibold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent">
|
||||
皮肤信息
|
||||
</h3>
|
||||
|
||||
{texture.description && (
|
||||
<motion.div
|
||||
className="bg-white/50 dark:bg-gray-700/50 rounded-lg p-3"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.9 }}
|
||||
>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{texture.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
{texture.uploader && (
|
||||
<motion.div
|
||||
className="flex justify-between items-center"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 1.0 }}
|
||||
>
|
||||
<span className="text-gray-600 dark:text-gray-400">上传者:</span>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">{texture.uploader.username}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{texture.created_at && (
|
||||
<motion.div
|
||||
className="flex justify-between items-center"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 1.1 }}
|
||||
>
|
||||
<span className="text-gray-600 dark:text-gray-400">上传时间:</span>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||
{new Date(texture.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="flex justify-between items-center"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
>
|
||||
<span className="text-gray-600 dark:text-gray-400">收藏数:</span>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">{texture.favorite_count || 0}</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex justify-between items-center"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 1.3 }}
|
||||
>
|
||||
<span className="text-gray-600 dark:text-gray-400">下载数:</span>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">{texture.download_count || 0}</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 快捷键提示 */}
|
||||
<motion.div
|
||||
className="bg-white/30 dark:bg-gray-700/30 rounded-lg p-3 text-xs text-gray-600 dark:text-gray-400"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.4 }}
|
||||
>
|
||||
<p className="font-medium mb-1">快捷键:</p>
|
||||
<p>空格 - 播放/暂停 | 1-4 - 切换动画 | M - 最小化 | ESC - 关闭</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 最小化时的内容 */}
|
||||
{isMinimized && (
|
||||
<motion.div
|
||||
className="flex items-center justify-center h-full p-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
animate={{ rotate: autoRotate ? 360 : 0 }}
|
||||
transition={{ rotate: { duration: 5, repeat: Infinity, ease: "linear" } }}
|
||||
className="w-20 h-20 mx-auto mb-2"
|
||||
>
|
||||
<SkinViewer
|
||||
skinUrl={texture.url}
|
||||
isSlim={texture.is_slim}
|
||||
width={80}
|
||||
height={80}
|
||||
autoRotate={autoRotate}
|
||||
walking={currentAnimation === 'walking'}
|
||||
running={currentAnimation === 'running'}
|
||||
jumping={currentAnimation === 'swimming'}
|
||||
/>
|
||||
</motion.div>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||
{texture.name}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user