diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index de0c4a7..9da4bca 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -106,7 +106,7 @@ export default function ProfilePage() { const [emailCodeCountdown, setEmailCodeCountdown] = useState(0); const [isChangingEmail, setIsChangingEmail] = useState(false); - const { user, isAuthenticated, logout, updateUser } = useAuth(); + const { user, isAuthenticated, isLoading: isAuthLoading, logout, updateUser } = useAuth(); // 加载用户数据 useEffect(() => { @@ -1099,6 +1099,27 @@ export default function ProfilePage() { ); }; + // auth 还在检查 token,先显示加载中,避免误判为未登录 + if (isAuthLoading) { + return ( +
+
+ +
+ +
+
+

加载中...

+

正在验证登录状态

+
+
+ ); + } + if (!isAuthenticated) { return (
diff --git a/src/app/skins/page.tsx b/src/app/skins/page.tsx index e6e1ae3..f36b2f0 100644 --- a/src/app/skins/page.tsx +++ b/src/app/skins/page.tsx @@ -3,8 +3,6 @@ import { useState, useEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { MagnifyingGlassIcon, EyeIcon, HeartIcon, ArrowDownTrayIcon, SparklesIcon, FunnelIcon, ArrowsUpDownIcon } from '@heroicons/react/24/outline'; -import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid'; -import SkinViewer from '@/components/SkinViewer'; import SkinDetailModal from '@/components/SkinDetailModal'; import SkinCard from '@/components/SkinCard'; import { searchTextures, toggleFavorite, type Texture } from '@/lib/api'; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index f2a0f9f..26938da 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -15,7 +15,7 @@ export default function Navbar() { const [navbarHeight, setNavbarHeight] = useState(0); const [scrollProgress, setScrollProgress] = useState(0); const navbarRef = useRef(null); - const { user, isAuthenticated, logout } = useAuth(); + const { user, isAuthenticated, isLoading: isAuthLoading, logout } = useAuth(); const router = useRouter(); const pathname = usePathname(); const { scrollY } = useScroll(); @@ -213,7 +213,12 @@ export default function Navbar() { ))} {/* 用户头像框 - 增强的微交互 */} - {isAuthenticated ? ( + {isAuthLoading ? ( +
+
+
+
+ ) : isAuthenticated ? (
(null); - const navigationTimeoutRef = useRef(null); - - // 监听路由变化 - useEffect(() => { - // 当 pathname 或 searchParams 变化时,表示路由发生了变化 - if (children !== displayChildren) { - setPendingChildren(children); - setIsNavigating(true); - - // 清除之前的超时 - if (navigationTimeoutRef.current) { - clearTimeout(navigationTimeoutRef.current); - } - - // 模拟加载时间,让 exit 动画有足够时间执行 - navigationTimeoutRef.current = setTimeout(() => { - setDisplayChildren(children); - setPendingChildren(null); - setIsNavigating(false); - }, 500); // 给 exit 动画 300ms + 缓冲时间 - } - }, [pathname, searchParams, children, displayChildren]); - - // 清理超时 - useEffect(() => { - return () => { - if (navigationTimeoutRef.current) { - clearTimeout(navigationTimeoutRef.current); - } - }; - }, []); - - const getPageVariants = (direction: 'left' | 'right' | 'up' | 'down' = 'right') => { - const directions = { - left: { x: -100, y: 0 }, - right: { x: 100, y: 0 }, - up: { x: 0, y: -100 }, - down: { x: 0, y: 100 } - }; - - const exitDirections = { - left: { x: 100, y: 0 }, - right: { x: -100, y: 0 }, - up: { x: 0, y: 100 }, - down: { x: 0, y: -100 } - }; - - return { - initial: { - opacity: 0, - ...directions[direction], - scale: 0.9, - rotateX: -15 - }, - animate: { - opacity: 1, - x: 0, - y: 0, - scale: 1, - rotateX: 0, - transition: { - duration: 0.5, - type: "spring" as const, - stiffness: 100, - damping: 15 - } - }, - exit: { - opacity: 0, - ...exitDirections[direction], - scale: 0.9, - rotateX: 15, - transition: { - duration: 0.3, - } - } - }; - }; - - const getLoadingVariants = () => ({ - initial: { - opacity: 0, - scale: 0.8, - y: 20 - }, - animate: { - opacity: 1, - scale: 1, - y: 0, - transition: { - duration: 0.3, - } - }, - exit: { - opacity: 0, - scale: 0.8, - y: -20, - transition: { - duration: 0.2, - } - } - }); - - return ( - <> - - {isNavigating && ( - -
- - - 页面切换中... - -
-
- )} -
- - - - {displayChildren} - - - - ); -} - -// 加载状态组件 -function PageTransitionFallback() { - return ( -
- {null} -
- ); -} - export default function PageTransition({ children }: PageTransitionProps) { + const pathname = usePathname(); + return ( - }> - {children} - + + {children} + ); } diff --git a/src/components/SkinDetailModal.tsx b/src/components/SkinDetailModal.tsx index d607efd..8ae6d14 100644 --- a/src/components/SkinDetailModal.tsx +++ b/src/components/SkinDetailModal.tsx @@ -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 && (
{/* 3D 预览区域 */} - -
- +
+
- +
{/* 控制面板 */} - {/* 动画控制 */} - ( - 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} - + ))}
@@ -440,7 +387,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr 快捷键

-
空格播放/暂停
+
空格暂停/继续
1-4切换动画
M最小化
ESC关闭
@@ -469,6 +416,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr walking={currentAnimation === 'walking'} running={currentAnimation === 'running'} jumping={currentAnimation === 'swimming'} + paused={isPaused} />

diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx index d5b56d6..839467a 100644 --- a/src/components/SkinViewer.tsx +++ b/src/components/SkinViewer.tsx @@ -17,6 +17,7 @@ interface SkinViewerProps { rotation?: boolean; // 新增:旋转控制 isExternalPreview?: boolean; // 新增:是否为外部预览 onImageLoaded?: () => void; // 新增:图片加载完成回调 + paused?: boolean; // 新增:暂停动画 } export default function SkinViewer({ @@ -33,6 +34,7 @@ export default function SkinViewer({ rotation = true, isExternalPreview = false, // 新增:默认为false onImageLoaded, // 新增:图片加载完成回调 + paused = false, // 新增:默认不暂停 }: SkinViewerProps) { const canvasRef = useRef(null); const viewerRef = useRef(null); @@ -141,6 +143,22 @@ export default function SkinViewer({ const viewer = viewerRef.current; + // 暂停动画时,使用 animation.paused 属性来暂停动画 + if (paused) { + if (viewer.animation) { + viewer.animation.paused = true; + } + viewer.autoRotate = false; + console.log('动画已暂停'); + return; + } else { + // 恢复动画 + if (viewer.animation) { + viewer.animation.paused = false; + console.log('动画已恢复'); + } + } + // 外部预览时只使用静止动画,禁用所有其他动画 if (isExternalPreview) { viewer.animation = new IdleAnimation(); @@ -170,7 +188,7 @@ export default function SkinViewer({ viewer.autoRotate = autoRotate && !walking && !running && !jumping; } - }, [walking, running, jumping, autoRotate, isExternalPreview]); + }, [walking, running, jumping, autoRotate, isExternalPreview, paused]); // 当皮肤URL改变时更新 useEffect(() => { diff --git a/src/components/profile/CharacterCard.tsx b/src/components/profile/CharacterCard.tsx index 6f61b2b..cdd9fbf 100644 --- a/src/components/profile/CharacterCard.tsx +++ b/src/components/profile/CharacterCard.tsx @@ -73,8 +73,8 @@ export default function CharacterCard({ ) : (