2025-12-04 22:33:46 +08:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-07-09 17:14:36 +08:00
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
|
import { XMarkIcon, PlayIcon } from '@heroicons/react/24/outline';
|
2025-12-04 22:33:46 +08:00
|
|
|
import SkinViewer from './SkinViewer';
|
|
|
|
|
|
|
|
|
|
interface SkinDetailModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
texture: {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
url: string;
|
|
|
|
|
type: 'SKIN' | 'CAPE';
|
|
|
|
|
is_slim?: boolean;
|
|
|
|
|
description?: string;
|
|
|
|
|
favorite_count?: number;
|
|
|
|
|
download_count?: number;
|
|
|
|
|
created_at?: string;
|
|
|
|
|
uploader?: {
|
|
|
|
|
username: string;
|
|
|
|
|
};
|
|
|
|
|
} | null;
|
2025-12-05 20:07:50 +08:00
|
|
|
isExternalPreview?: boolean;
|
2025-12-04 22:33:46 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPreview = false }: SkinDetailModalProps) {
|
|
|
|
|
const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'swimming'>('idle');
|
|
|
|
|
const [autoRotate, setAutoRotate] = useState(!isExternalPreview);
|
2025-12-04 22:33:46 +08:00
|
|
|
const [rotation, setRotation] = useState(true);
|
2025-12-05 20:07:50 +08:00
|
|
|
const [isMinimized, setIsMinimized] = useState(false);
|
2026-07-09 17:14:36 +08:00
|
|
|
const [isPaused, setIsPaused] = useState(false); // 新增:动画暂停状态
|
2025-12-05 20:07:50 +08:00
|
|
|
|
2025-12-04 22:33:46 +08:00
|
|
|
// 重置状态当对话框关闭时
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
setCurrentAnimation('idle');
|
2026-07-09 17:14:36 +08:00
|
|
|
setAutoRotate(!isExternalPreview);
|
2025-12-04 22:33:46 +08:00
|
|
|
setRotation(true);
|
2025-12-05 20:07:50 +08:00
|
|
|
setIsMinimized(false);
|
2026-07-09 17:14:36 +08:00
|
|
|
setIsPaused(false); // 重置暂停状态
|
2025-12-04 22:33:46 +08:00
|
|
|
}
|
|
|
|
|
}, [isOpen]);
|
|
|
|
|
|
|
|
|
|
// 键盘事件处理
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
|
|
|
|
|
switch (e.key) {
|
|
|
|
|
case 'Escape':
|
|
|
|
|
onClose();
|
|
|
|
|
break;
|
|
|
|
|
case ' ':
|
|
|
|
|
e.preventDefault();
|
2026-07-09 17:14:36 +08:00
|
|
|
// 空格键暂停/继续动画
|
|
|
|
|
setIsPaused(prev => !prev);
|
2025-12-04 22:33:46 +08:00
|
|
|
break;
|
|
|
|
|
case '1':
|
|
|
|
|
setCurrentAnimation('idle');
|
|
|
|
|
break;
|
|
|
|
|
case '2':
|
|
|
|
|
setCurrentAnimation('walking');
|
|
|
|
|
break;
|
|
|
|
|
case '3':
|
|
|
|
|
setCurrentAnimation('running');
|
|
|
|
|
break;
|
|
|
|
|
case '4':
|
2025-12-05 20:07:50 +08:00
|
|
|
setCurrentAnimation('swimming');
|
|
|
|
|
break;
|
|
|
|
|
case 'm':
|
|
|
|
|
case 'M':
|
2026-07-09 17:14:36 +08:00
|
|
|
setIsMinimized(prev => !prev);
|
2025-12-04 22:33:46 +08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
2026-07-09 17:14:36 +08:00
|
|
|
}, [isOpen, onClose]);
|
2025-12-05 20:07:50 +08:00
|
|
|
|
|
|
|
|
// 动画控制函数
|
|
|
|
|
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,
|
2026-01-09 17:44:21 +08:00
|
|
|
type: "spring" as const,
|
2025-12-05 20:07:50 +08:00
|
|
|
stiffness: 100,
|
|
|
|
|
damping: 15
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
exit: {
|
|
|
|
|
opacity: 0,
|
|
|
|
|
scale: 0.7,
|
|
|
|
|
rotateX: 15,
|
|
|
|
|
y: 50,
|
|
|
|
|
transition: {
|
2026-01-09 17:44:21 +08:00
|
|
|
duration: 0.3
|
2025-12-05 20:07:50 +08:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
minimized: {
|
|
|
|
|
scale: 0.8,
|
|
|
|
|
y: window.innerHeight - 200,
|
|
|
|
|
x: window.innerWidth - 300,
|
|
|
|
|
width: 280,
|
|
|
|
|
height: 150,
|
|
|
|
|
transition: {
|
2026-01-09 17:44:21 +08:00
|
|
|
duration: 0.4
|
2025-12-05 20:07:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-04 22:33:46 +08:00
|
|
|
if (!texture) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{isOpen && (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
exit={{ opacity: 0 }}
|
2025-12-05 20:07:50 +08:00
|
|
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
2025-12-04 22:33:46 +08:00
|
|
|
onClick={onClose}
|
2025-12-05 20:07:50 +08:00
|
|
|
style={{
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
isolation: 'isolate'
|
|
|
|
|
}}
|
2025-12-04 22:33:46 +08:00
|
|
|
>
|
|
|
|
|
<motion.div
|
2025-12-05 20:07:50 +08:00
|
|
|
variants={getModalVariants()}
|
|
|
|
|
initial="initial"
|
|
|
|
|
animate={isMinimized ? "minimized" : "animate"}
|
|
|
|
|
exit="exit"
|
2025-12-04 22:33:46 +08:00
|
|
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
2026-01-09 17:44:21 +08:00
|
|
|
className={`relative bg-white/98 dark:bg-gray-800/98 rounded-2xl shadow-2xl overflow-hidden border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl ${
|
2025-12-05 20:07:50 +08:00
|
|
|
isMinimized ? 'fixed bottom-4 right-4' : 'w-full max-w-6xl h-[90vh]'
|
|
|
|
|
}`}
|
2025-12-04 22:33:46 +08:00
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
2025-12-05 20:07:50 +08:00
|
|
|
<motion.div
|
2026-01-09 17:44:21 +08:00
|
|
|
className="absolute top-0 left-0 right-0 bg-gradient-to-r from-white/95 via-orange-50/95 to-amber-50/95 dark:from-gray-800/95 dark:via-gray-800/95 dark:to-gray-700/95 backdrop-blur-xl border-b border-orange-200/60 dark:border-gray-600/60 p-4 z-10 shadow-lg"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ y: -100 }}
|
|
|
|
|
animate={{ y: 0 }}
|
|
|
|
|
transition={{ delay: 0.2, duration: 0.4 }}
|
|
|
|
|
>
|
2025-12-04 22:33:46 +08:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-4">
|
2025-12-05 20:07:50 +08:00
|
|
|
<motion.h2
|
2026-01-09 17:44:21 +08:00
|
|
|
className="text-3xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent drop-shadow-sm"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
transition={{ delay: 0.3 }}
|
|
|
|
|
>
|
2025-12-04 22:33:46 +08:00
|
|
|
{texture.name}
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.h2>
|
2025-12-04 22:33:46 +08:00
|
|
|
<div className="flex items-center space-x-2">
|
2025-12-05 20:07:50 +08:00
|
|
|
<motion.span
|
2026-01-09 17:44:21 +08:00
|
|
|
className={`px-4 py-2 text-sm rounded-full font-semibold shadow-md ${
|
2025-12-05 20:07:50 +08:00
|
|
|
texture.type === 'SKIN'
|
2026-01-09 17:44:21 +08:00
|
|
|
? 'bg-gradient-to-r from-orange-100 to-amber-100 text-orange-800 dark:from-orange-900/40 dark:to-amber-900/40 dark:text-orange-300 border border-orange-200/50 dark:border-orange-700/30'
|
|
|
|
|
: 'bg-gradient-to-r from-purple-100 to-pink-100 text-purple-800 dark:from-purple-900/40 dark:to-pink-900/40 dark:text-purple-300 border border-purple-200/50 dark:border-purple-700/30'
|
2025-12-05 20:07:50 +08:00
|
|
|
}`}
|
|
|
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
|
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
|
|
|
transition={{ delay: 0.4, type: "spring", stiffness: 200 }}
|
|
|
|
|
>
|
2025-12-04 22:33:46 +08:00
|
|
|
{texture.type === 'SKIN' ? '皮肤' : '披风'}
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.span>
|
2025-12-04 22:33:46 +08:00
|
|
|
{texture.is_slim && (
|
2026-01-09 17:44:21 +08:00
|
|
|
<motion.span
|
|
|
|
|
className="px-4 py-2 bg-gradient-to-r from-pink-100 to-rose-100 text-pink-800 dark:from-pink-900/40 dark:to-rose-900/40 text-sm rounded-full font-semibold shadow-md border border-pink-200/50 dark:border-pink-700/30"
|
|
|
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
|
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
|
|
|
transition={{ delay: 0.5, type: "spring", stiffness: 200 }}
|
|
|
|
|
>
|
2025-12-04 22:33:46 +08:00
|
|
|
细臂
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.span>
|
2025-12-04 22:33:46 +08:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
<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}
|
2026-01-09 17:44:21 +08:00
|
|
|
className="p-3 text-gray-500 hover:text-red-500 dark:text-gray-400 dark:hover:text-red-400 rounded-full hover:bg-red-100 dark:hover:bg-red-900/30 transition-all duration-300 shadow-md hover:shadow-lg"
|
|
|
|
|
whileHover={{ scale: 1.15, rotate: 90 }}
|
|
|
|
|
whileTap={{ scale: 0.85 }}
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, rotate: -180 }}
|
|
|
|
|
animate={{ opacity: 1, rotate: 0 }}
|
|
|
|
|
transition={{ delay: 0.7 }}
|
|
|
|
|
>
|
|
|
|
|
<XMarkIcon className="w-6 h-6" />
|
|
|
|
|
</motion.button>
|
2025-12-04 22:33:46 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
{!isMinimized && (
|
|
|
|
|
<div className="flex h-full pt-20">
|
|
|
|
|
{/* 3D 预览区域 */}
|
2026-07-09 17:14:36 +08:00
|
|
|
<motion.div
|
2026-01-09 17:44:21 +08:00
|
|
|
className="flex-1 flex items-center justify-center p-8 bg-gradient-to-br from-orange-50/40 via-amber-50/40 to-yellow-50/40 dark:from-gray-900/40 dark:via-gray-800/40 dark:to-gray-700/40"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
|
|
transition={{ delay: 0.3, duration: 0.5 }}
|
|
|
|
|
>
|
2026-07-09 17:14:36 +08:00
|
|
|
<div className="w-full h-full max-w-2xl max-h-2xl flex items-center justify-center">
|
|
|
|
|
<div className="relative">
|
2026-01-09 17:44:21 +08:00
|
|
|
<div className="absolute inset-0 bg-gradient-to-br from-orange-400/20 to-amber-400/20 dark:from-orange-500/10 dark:to-amber-500/10 rounded-2xl blur-xl"></div>
|
2025-12-05 20:07:50 +08:00
|
|
|
<SkinViewer
|
|
|
|
|
skinUrl={texture.url}
|
|
|
|
|
isSlim={texture.is_slim}
|
2026-07-09 17:14:36 +08:00
|
|
|
width={500}
|
|
|
|
|
height={500}
|
2026-02-24 11:09:37 +08:00
|
|
|
className="rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10"
|
2026-07-09 17:14:36 +08:00
|
|
|
autoRotate={autoRotate && currentAnimation === 'idle'}
|
2025-12-05 20:07:50 +08:00
|
|
|
walking={currentAnimation === 'walking'}
|
|
|
|
|
running={currentAnimation === 'running'}
|
|
|
|
|
jumping={currentAnimation === 'swimming'}
|
|
|
|
|
rotation={rotation}
|
2026-07-09 17:14:36 +08:00
|
|
|
paused={isPaused}
|
2025-12-05 20:07:50 +08:00
|
|
|
/>
|
2026-07-09 17:14:36 +08:00
|
|
|
</div>
|
2025-12-04 22:33:46 +08:00
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
{/* 控制面板 */}
|
2026-07-09 17:14:36 +08:00
|
|
|
<motion.div
|
2026-01-09 17:44:21 +08:00
|
|
|
className="w-80 bg-gradient-to-b from-orange-50/90 via-amber-50/90 to-yellow-50/90 dark:from-gray-900/90 dark:via-gray-800/90 dark:to-gray-700/90 backdrop-blur-xl border-l border-orange-200/60 dark:border-gray-600/60 p-6 space-y-6 overflow-y-auto custom-scrollbar shadow-inner"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, x: 50 }}
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
transition={{ delay: 0.4, duration: 0.5 }}
|
|
|
|
|
>
|
|
|
|
|
{/* 动画控制 */}
|
|
|
|
|
<motion.div className="space-y-4">
|
2026-07-09 17:14:36 +08:00
|
|
|
<motion.h3
|
2026-01-09 17:44:21 +08:00
|
|
|
className="text-xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent flex items-center drop-shadow-sm"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.5 }}
|
2025-12-04 22:33:46 +08:00
|
|
|
>
|
2026-01-09 17:44:21 +08:00
|
|
|
<PlayIcon className="w-6 h-6 mr-3" />
|
2025-12-05 20:07:50 +08:00
|
|
|
动画控制
|
|
|
|
|
</motion.h3>
|
2025-12-04 22:33:46 +08:00
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
<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) => (
|
2026-07-09 17:14:36 +08:00
|
|
|
<button
|
2025-12-05 20:07:50 +08:00
|
|
|
key={anim.key}
|
|
|
|
|
onClick={() => handleAnimationChange(anim.key as any)}
|
2026-07-09 17:14:36 +08:00
|
|
|
className={`p-4 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
2025-12-05 20:07:50 +08:00
|
|
|
currentAnimation === anim.key
|
2026-07-09 17:14:36 +08:00
|
|
|
? 'bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 text-white shadow-lg ring-2 ring-orange-300 dark:ring-orange-500'
|
|
|
|
|
: 'bg-white/80 dark:bg-gray-700/80 text-gray-700 dark:text-gray-300 hover:bg-orange-50 dark:hover:bg-orange-900/30 border border-orange-200/50 dark:border-gray-600 hover:shadow-md'
|
2025-12-05 20:07:50 +08:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{anim.label}
|
2026-07-09 17:14:36 +08:00
|
|
|
</button>
|
2025-12-05 20:07:50 +08:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
{/* 信息面板 */}
|
|
|
|
|
<motion.div
|
|
|
|
|
className="space-y-4"
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
transition={{ delay: 0.8 }}
|
|
|
|
|
>
|
2026-01-09 17:44:21 +08:00
|
|
|
<h3 className="text-xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent drop-shadow-sm">
|
2025-12-05 20:07:50 +08:00
|
|
|
皮肤信息
|
|
|
|
|
</h3>
|
|
|
|
|
|
2025-12-04 22:33:46 +08:00
|
|
|
{texture.description && (
|
2025-12-05 20:07:50 +08:00
|
|
|
<motion.div
|
2026-01-09 17:44:21 +08:00
|
|
|
className="bg-white/70 dark:bg-gray-700/70 rounded-xl p-4 shadow-md border border-white/30 dark:border-gray-600/30"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ delay: 0.9 }}
|
|
|
|
|
>
|
2026-01-09 17:44:21 +08:00
|
|
|
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed font-medium">
|
2025-12-05 20:07:50 +08:00
|
|
|
{texture.description}
|
|
|
|
|
</p>
|
|
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
)}
|
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
<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>
|
2025-12-04 22:33:46 +08:00
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
{/* 快捷键提示 */}
|
|
|
|
|
<motion.div
|
2026-01-09 17:44:21 +08:00
|
|
|
className="bg-gradient-to-r from-orange-100/50 to-amber-100/50 dark:from-gray-700/50 dark:to-gray-600/50 rounded-xl p-4 text-xs text-gray-600 dark:text-gray-400 border border-orange-200/30 dark:border-gray-600/30"
|
2025-12-05 20:07:50 +08:00
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
transition={{ delay: 1.4 }}
|
|
|
|
|
>
|
2026-01-09 17:44:21 +08:00
|
|
|
<p className="font-bold mb-2 text-orange-700 dark:text-orange-300 flex items-center">
|
|
|
|
|
<span className="w-2 h-2 bg-orange-500 rounded-full mr-2"></span>
|
|
|
|
|
快捷键
|
|
|
|
|
</p>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
2026-07-09 17:14:36 +08:00
|
|
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">空格</kbd>暂停/继续</div>
|
2026-01-09 17:44:21 +08:00
|
|
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">1-4</kbd>切换动画</div>
|
|
|
|
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">M</kbd>最小化</div>
|
|
|
|
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">ESC</kbd>关闭</div>
|
|
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
</motion.div>
|
|
|
|
|
</motion.div>
|
2025-12-04 22:33:46 +08:00
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 最小化时的内容 */}
|
|
|
|
|
{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">
|
2026-01-09 17:44:21 +08:00
|
|
|
<div className="w-20 h-20 mx-auto mb-2">
|
2025-12-05 20:07:50 +08:00
|
|
|
<SkinViewer
|
|
|
|
|
skinUrl={texture.url}
|
|
|
|
|
isSlim={texture.is_slim}
|
|
|
|
|
width={80}
|
|
|
|
|
height={80}
|
|
|
|
|
autoRotate={autoRotate}
|
|
|
|
|
walking={currentAnimation === 'walking'}
|
|
|
|
|
running={currentAnimation === 'running'}
|
|
|
|
|
jumping={currentAnimation === 'swimming'}
|
2026-07-09 17:14:36 +08:00
|
|
|
paused={isPaused}
|
2025-12-05 20:07:50 +08:00
|
|
|
/>
|
2026-01-09 17:44:21 +08:00
|
|
|
</div>
|
2025-12-05 20:07:50 +08:00
|
|
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
|
|
|
|
{texture.name}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
2025-12-04 22:33:46 +08:00
|
|
|
</motion.div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
);
|
2025-12-05 20:07:50 +08:00
|
|
|
}
|