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

@@ -106,7 +106,7 @@ export default function ProfilePage() {
const [emailCodeCountdown, setEmailCodeCountdown] = useState(0); const [emailCodeCountdown, setEmailCodeCountdown] = useState(0);
const [isChangingEmail, setIsChangingEmail] = useState(false); const [isChangingEmail, setIsChangingEmail] = useState(false);
const { user, isAuthenticated, logout, updateUser } = useAuth(); const { user, isAuthenticated, isLoading: isAuthLoading, logout, updateUser } = useAuth();
// 加载用户数据 // 加载用户数据
useEffect(() => { useEffect(() => {
@@ -1099,6 +1099,27 @@ export default function ProfilePage() {
); );
}; };
// auth 还在检查 token先显示加载中避免误判为未登录
if (isAuthLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="text-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="mb-8"
>
<div className="w-16 h-16 bg-gradient-to-br from-orange-400 via-amber-500 to-orange-600 rounded-full flex items-center justify-center shadow-xl mx-auto">
<Cog6ToothIcon className="w-8 h-8 text-white" />
</div>
</motion.div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">...</h2>
<p className="text-gray-600 dark:text-gray-400"></p>
</div>
</div>
);
}
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">

View File

@@ -3,8 +3,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { MagnifyingGlassIcon, EyeIcon, HeartIcon, ArrowDownTrayIcon, SparklesIcon, FunnelIcon, ArrowsUpDownIcon } from '@heroicons/react/24/outline'; 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 SkinDetailModal from '@/components/SkinDetailModal';
import SkinCard from '@/components/SkinCard'; import SkinCard from '@/components/SkinCard';
import { searchTextures, toggleFavorite, type Texture } from '@/lib/api'; import { searchTextures, toggleFavorite, type Texture } from '@/lib/api';

View File

@@ -15,7 +15,7 @@ export default function Navbar() {
const [navbarHeight, setNavbarHeight] = useState(0); const [navbarHeight, setNavbarHeight] = useState(0);
const [scrollProgress, setScrollProgress] = useState(0); const [scrollProgress, setScrollProgress] = useState(0);
const navbarRef = useRef<HTMLElement>(null); const navbarRef = useRef<HTMLElement>(null);
const { user, isAuthenticated, logout } = useAuth(); const { user, isAuthenticated, isLoading: isAuthLoading, logout } = useAuth();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const { scrollY } = useScroll(); const { scrollY } = useScroll();
@@ -213,7 +213,12 @@ export default function Navbar() {
))} ))}
{/* 用户头像框 - 增强的微交互 */} {/* 用户头像框 - 增强的微交互 */}
{isAuthenticated ? ( {isAuthLoading ? (
<div className="flex items-center space-x-3">
<div className="w-9 h-9 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse" />
<div className="w-12 h-4 rounded bg-gray-200 dark:bg-gray-700 animate-pulse" />
</div>
) : isAuthenticated ? (
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<motion.div <motion.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}

View File

@@ -1,191 +1,23 @@
'use client'; 'use client';
import { motion, AnimatePresence } from 'framer-motion'; import { motion } from 'framer-motion';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useEffect, useState, useRef, Suspense } from 'react';
import { useRouter } from 'next/navigation';
interface PageTransitionProps { interface PageTransitionProps {
children: React.ReactNode; children: React.ReactNode;
} }
// 内部组件:使用 useSearchParams 的部分
function PageTransitionContent({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const [isNavigating, setIsNavigating] = useState(false);
const [displayChildren, setDisplayChildren] = useState(children);
const [pendingChildren, setPendingChildren] = useState<React.ReactNode | null>(null);
const navigationTimeoutRef = useRef<NodeJS.Timeout | null>(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 (
<>
<AnimatePresence mode="wait">
{isNavigating && (
<motion.div
key="loading"
variants={getLoadingVariants()}
initial="initial"
animate="animate"
exit="exit"
className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm pointer-events-none"
>
<div className="text-center">
<motion.div
animate={{
rotate: 360,
scale: [1, 1.1, 1]
}}
transition={{
rotate: { duration: 1, repeat: Infinity },
scale: { duration: 1.5, repeat: Infinity }
}}
className="w-12 h-12 border-4 border-orange-500 border-t-transparent rounded-full mx-auto mb-4"
/>
<motion.p
className="text-lg font-medium text-gray-700 dark:text-gray-300"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
...
</motion.p>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
<motion.div
key={pathname + searchParams.toString()}
variants={getPageVariants()}
initial="initial"
animate="animate"
exit="exit"
className="min-h-screen"
>
{displayChildren}
</motion.div>
</AnimatePresence>
</>
);
}
// 加载状态组件
function PageTransitionFallback() {
return (
<div className="min-h-screen">
{null}
</div>
);
}
export default function PageTransition({ children }: PageTransitionProps) { export default function PageTransition({ children }: PageTransitionProps) {
const pathname = usePathname();
return ( return (
<Suspense fallback={<PageTransitionFallback />}> <motion.div
<PageTransitionContent>{children}</PageTransitionContent> key={pathname}
</Suspense> initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
{children}
</motion.div>
); );
} }

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { motion, AnimatePresence, useSpring, useTransform } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { XMarkIcon, PlayIcon, PauseIcon, ArrowPathIcon, ForwardIcon } from '@heroicons/react/24/outline'; import { XMarkIcon, PlayIcon } from '@heroicons/react/24/outline';
import SkinViewer from './SkinViewer'; import SkinViewer from './SkinViewer';
interface SkinDetailModalProps { interface SkinDetailModalProps {
@@ -26,27 +26,20 @@ interface SkinDetailModalProps {
} }
export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPreview = false }: 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 [currentAnimation, setCurrentAnimation] = useState<'idle' | 'walking' | 'running' | 'swimming'>('idle');
const [autoRotate, setAutoRotate] = useState(!isExternalPreview); const [autoRotate, setAutoRotate] = useState(!isExternalPreview);
const [rotation, setRotation] = useState(true); const [rotation, setRotation] = useState(true);
const [isMinimized, setIsMinimized] = useState(false); 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(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setIsPlaying(false);
setCurrentAnimation('idle'); setCurrentAnimation('idle');
setAutoRotate(true); setAutoRotate(!isExternalPreview);
setRotation(true); setRotation(true);
setIsMinimized(false); setIsMinimized(false);
setActiveTab('preview'); setIsPaused(false); // 重置暂停状态
} }
}, [isOpen]); }, [isOpen]);
@@ -61,7 +54,8 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
break; break;
case ' ': case ' ':
e.preventDefault(); e.preventDefault();
setIsPlaying(!isPlaying); // 空格键暂停/继续动画
setIsPaused(prev => !prev);
break; break;
case '1': case '1':
setCurrentAnimation('idle'); setCurrentAnimation('idle');
@@ -77,14 +71,14 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
break; break;
case 'm': case 'm':
case 'M': case 'M':
setIsMinimized(!isMinimized); setIsMinimized(prev => !prev);
break; break;
} }
}; };
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose, isPlaying, isMinimized]); }, [isOpen, onClose]);
// 动画控制函数 // 动画控制函数
const handleAnimationChange = (animation: 'idle' | 'walking' | 'running' | 'swimming') => { 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; if (!texture) return null;
return ( return (
@@ -272,43 +235,34 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
{!isMinimized && ( {!isMinimized && (
<div className="flex h-full pt-20"> <div className="flex h-full pt-20">
{/* 3D 预览区域 */} {/* 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" 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 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, duration: 0.5 }} transition={{ delay: 0.3, duration: 0.5 }}
> >
<div className="w-full h-full max-w-2xl max-h-2xl"> <div className="w-full h-full max-w-2xl max-h-2xl flex items-center justify-center">
<motion.div <div className="relative">
animate={{
scale: isPlaying ? 1.02 : 1,
y: isPlaying ? -5 : 0
}}
transition={{
scale: { duration: 0.2 },
y: { duration: 0.2 }
}}
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> <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 <SkinViewer
skinUrl={texture.url} skinUrl={texture.url}
isSlim={texture.is_slim} isSlim={texture.is_slim}
width={600} width={500}
height={600} height={500}
className="rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10" 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'} walking={currentAnimation === 'walking'}
running={currentAnimation === 'running'} running={currentAnimation === 'running'}
jumping={currentAnimation === 'swimming'} jumping={currentAnimation === 'swimming'}
rotation={rotation} rotation={rotation}
paused={isPaused}
/> />
</motion.div> </div>
</div> </div>
</motion.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" 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 }} initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
@@ -316,7 +270,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
> >
{/* 动画控制 */} {/* 动画控制 */}
<motion.div className="space-y-4"> <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" 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 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -333,24 +287,17 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
{ key: 'running', label: '跑步', icon: null }, { key: 'running', label: '跑步', icon: null },
{ key: 'swimming', label: '游泳', icon: null } { key: 'swimming', label: '游泳', icon: null }
].map((anim, i) => ( ].map((anim, i) => (
<motion.button <button
key={anim.key} key={anim.key}
variants={getAnimationButtonVariants(currentAnimation === anim.key)}
animate={currentAnimation === anim.key ? "active" : "animate"}
whileHover="hover"
whileTap="tap"
onClick={() => handleAnimationChange(anim.key as any)} 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 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-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:border-orange-300 dark:hover:border-orange-400 hover:scale-105 hover:shadow-lg' : '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} {anim.label}
</motion.button> </button>
))} ))}
</div> </div>
</motion.div> </motion.div>
@@ -440,7 +387,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
</p> </p>
<div className="grid grid-cols-2 gap-2 text-xs"> <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">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">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 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'} walking={currentAnimation === 'walking'}
running={currentAnimation === 'running'} running={currentAnimation === 'running'}
jumping={currentAnimation === 'swimming'} jumping={currentAnimation === 'swimming'}
paused={isPaused}
/> />
</div> </div>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate"> <p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">

View File

@@ -17,6 +17,7 @@ interface SkinViewerProps {
rotation?: boolean; // 新增:旋转控制 rotation?: boolean; // 新增:旋转控制
isExternalPreview?: boolean; // 新增:是否为外部预览 isExternalPreview?: boolean; // 新增:是否为外部预览
onImageLoaded?: () => void; // 新增:图片加载完成回调 onImageLoaded?: () => void; // 新增:图片加载完成回调
paused?: boolean; // 新增:暂停动画
} }
export default function SkinViewer({ export default function SkinViewer({
@@ -33,6 +34,7 @@ export default function SkinViewer({
rotation = true, rotation = true,
isExternalPreview = false, // 新增默认为false isExternalPreview = false, // 新增默认为false
onImageLoaded, // 新增:图片加载完成回调 onImageLoaded, // 新增:图片加载完成回调
paused = false, // 新增:默认不暂停
}: SkinViewerProps) { }: SkinViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const viewerRef = useRef<SkinViewer3D | null>(null); const viewerRef = useRef<SkinViewer3D | null>(null);
@@ -141,6 +143,22 @@ export default function SkinViewer({
const viewer = viewerRef.current; 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) { if (isExternalPreview) {
viewer.animation = new IdleAnimation(); viewer.animation = new IdleAnimation();
@@ -170,7 +188,7 @@ export default function SkinViewer({
viewer.autoRotate = autoRotate && !walking && !running && !jumping; viewer.autoRotate = autoRotate && !walking && !running && !jumping;
} }
}, [walking, running, jumping, autoRotate, isExternalPreview]); }, [walking, running, jumping, autoRotate, isExternalPreview, paused]);
// 当皮肤URL改变时更新 // 当皮肤URL改变时更新
useEffect(() => { useEffect(() => {

View File

@@ -73,8 +73,8 @@ export default function CharacterCard({
<SkinViewer <SkinViewer
skinUrl={skinUrl} skinUrl={skinUrl}
isSlim={isSlim} isSlim={isSlim}
width={180} width={120}
height={180} height={120}
autoRotate={false} autoRotate={false}
/> />
) : ( ) : (