forked from CarrotSkin/carrotskin
- 在用户资料页面添加皮肤选择和管理功能,支持上传、配置和移除皮肤 - 实现Yggdrasil密码重置功能,用户可生成新密码并显示 - 优化皮肤展示和选择界面,增强用户体验 - 更新SkinViewer组件,支持跑步和跳跃动画
316 lines
14 KiB
TypeScript
316 lines
14 KiB
TypeScript
'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 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;
|
||
}
|
||
|
||
export default function SkinDetailModal({ isOpen, onClose, texture }: SkinDetailModalProps) {
|
||
const [isPlaying, setIsPlaying] = useState(false);
|
||
const [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'jumping'>('idle');
|
||
const [autoRotate, setAutoRotate] = useState(true);
|
||
const [rotation, setRotation] = useState(true);
|
||
|
||
// 重置状态当对话框关闭时
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
setIsPlaying(false);
|
||
setCurrentAnimation('idle');
|
||
setAutoRotate(true);
|
||
setRotation(true);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 键盘事件处理
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
if (!isOpen) return;
|
||
|
||
switch (e.key) {
|
||
case 'Escape':
|
||
onClose();
|
||
break;
|
||
case ' ':
|
||
e.preventDefault();
|
||
setIsPlaying(!isPlaying);
|
||
break;
|
||
case '1':
|
||
setCurrentAnimation('idle');
|
||
break;
|
||
case '2':
|
||
setCurrentAnimation('walking');
|
||
break;
|
||
case '3':
|
||
setCurrentAnimation('running');
|
||
break;
|
||
case '4':
|
||
setCurrentAnimation('jumping');
|
||
break;
|
||
}
|
||
};
|
||
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, [isOpen, onClose, isPlaying]);
|
||
|
||
if (!texture) return null;
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<motion.div
|
||
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"
|
||
onClick={onClose}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
exit={{ scale: 0.9, opacity: 0 }}
|
||
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"
|
||
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">
|
||
<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">
|
||
{texture.name}
|
||
</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'
|
||
}`}>
|
||
{texture.type === 'SKIN' ? '皮肤' : '披风'}
|
||
</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">
|
||
细臂
|
||
</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 }}
|
||
>
|
||
<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 }}
|
||
>
|
||
静止
|
||
</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>
|
||
</div>
|
||
</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 }}
|
||
>
|
||
<ArrowPathIcon className="w-4 h-4 mr-2" />
|
||
{autoRotate ? '停止旋转' : '自动旋转'}
|
||
</motion.button>
|
||
|
||
<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>
|
||
|
||
{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>
|
||
|
||
{/* 快捷键提示 */}
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|