fix(ui): improve authentication UX and refine skin viewer components
- Add loading states to `ProfilePage` and `Navbar` to prevent flickering during authentication checks - Implement animation pause/resume functionality in `SkinDetailModal` and `SkinViewer` - Simplify `PageTransition` component by removing unnecessary complexity and state - Refactor `SkinDetailModal` to use a more robust state management for animations - Adjust `CharacterCard` skin dimensions for better layout consistency - Clean up unused imports and redundant animation variants in several components
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion';
|
||||
import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon } from '@heroicons/react/24/outline';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { XMarkIcon, PlayIcon } from '@heroicons/react/24/outline';
|
||||
import SkinViewer from './SkinViewer';
|
||||
|
||||
interface SkinDetailModalProps {
|
||||
@@ -26,27 +26,20 @@ interface SkinDetailModalProps {
|
||||
}
|
||||
|
||||
export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPreview = false }: SkinDetailModalProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
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 [isPaused, setIsPaused] = useState(false); // 新增:动画暂停状态
|
||||
|
||||
// 弹簧动画配置
|
||||
const springConfig = { stiffness: 300, damping: 30 };
|
||||
const scale = useSpring(1, springConfig);
|
||||
const rotate = useSpring(0, springConfig);
|
||||
|
||||
// 重置状态当对话框关闭时
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setIsPlaying(false);
|
||||
setCurrentAnimation('idle');
|
||||
setAutoRotate(true);
|
||||
setAutoRotate(!isExternalPreview);
|
||||
setRotation(true);
|
||||
setIsMinimized(false);
|
||||
setActiveTab('preview');
|
||||
setIsPaused(false); // 重置暂停状态
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -61,7 +54,8 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
break;
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
setIsPlaying(!isPlaying);
|
||||
// 空格键暂停/继续动画
|
||||
setIsPaused(prev => !prev);
|
||||
break;
|
||||
case '1':
|
||||
setCurrentAnimation('idle');
|
||||
@@ -77,14 +71,14 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
setIsMinimized(!isMinimized);
|
||||
setIsMinimized(prev => !prev);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose, isPlaying, isMinimized]);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 动画控制函数
|
||||
const handleAnimationChange = (animation: 'idle' | 'walking' | 'running' | 'swimming') => {
|
||||
@@ -135,37 +129,6 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
}
|
||||
});
|
||||
|
||||
const getAnimationButtonVariants = (isActive: boolean) => ({
|
||||
initial: { scale: 0.9, opacity: 0.8 },
|
||||
animate: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
type: "spring" as const,
|
||||
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" as const,
|
||||
stiffness: 400,
|
||||
damping: 25
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!texture) return null;
|
||||
|
||||
return (
|
||||
@@ -272,43 +235,34 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
{!isMinimized && (
|
||||
<div className="flex h-full pt-20">
|
||||
{/* 3D 预览区域 */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
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"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<div className="w-full h-full max-w-2xl max-h-2xl">
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: isPlaying ? 1.02 : 1,
|
||||
y: isPlaying ? -5 : 0
|
||||
}}
|
||||
transition={{
|
||||
scale: { duration: 0.2 },
|
||||
y: { duration: 0.2 }
|
||||
}}
|
||||
className="relative"
|
||||
>
|
||||
<div className="w-full h-full max-w-2xl max-h-2xl flex items-center justify-center">
|
||||
<div className="relative">
|
||||
<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>
|
||||
<SkinViewer
|
||||
skinUrl={texture.url}
|
||||
isSlim={texture.is_slim}
|
||||
width={600}
|
||||
height={600}
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10"
|
||||
autoRotate={autoRotate}
|
||||
autoRotate={autoRotate && currentAnimation === 'idle'}
|
||||
walking={currentAnimation === 'walking'}
|
||||
running={currentAnimation === 'running'}
|
||||
jumping={currentAnimation === 'swimming'}
|
||||
rotation={rotation}
|
||||
paused={isPaused}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 控制面板 */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
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"
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
@@ -316,7 +270,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
>
|
||||
{/* 动画控制 */}
|
||||
<motion.div className="space-y-4">
|
||||
<motion.h3
|
||||
<motion.h3
|
||||
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"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -333,24 +287,17 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
{ key: 'running', label: '跑步', icon: null },
|
||||
{ key: 'swimming', label: '游泳', icon: null }
|
||||
].map((anim, i) => (
|
||||
<motion.button
|
||||
<button
|
||||
key={anim.key}
|
||||
variants={getAnimationButtonVariants(currentAnimation === anim.key)}
|
||||
animate={currentAnimation === anim.key ? "active" : "animate"}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onClick={() => handleAnimationChange(anim.key as any)}
|
||||
className={`p-4 rounded-xl text-sm font-semibold transition-all duration-300 transform ${
|
||||
className={`p-4 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
||||
currentAnimation === anim.key
|
||||
? 'bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 text-white shadow-2xl scale-105 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:border-orange-300 dark:hover:border-orange-400 hover:scale-105 hover:shadow-lg'
|
||||
? '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'
|
||||
}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
||||
transition={{ delay: 0.6 + i * 0.1 }}
|
||||
>
|
||||
{anim.label}
|
||||
</motion.button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -440,7 +387,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
快捷键
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -469,6 +416,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
||||
walking={currentAnimation === 'walking'}
|
||||
running={currentAnimation === 'running'}
|
||||
jumping={currentAnimation === 'swimming'}
|
||||
paused={isPaused}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||
|
||||
Reference in New Issue
Block a user