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:
2026-07-09 17:14:36 +08:00
parent 268344c357
commit 38f11445ba
7 changed files with 88 additions and 266 deletions

View File

@@ -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">