Files
carrotskin/src/components/PageTransition.tsx

178 lines
4.7 KiB
TypeScript

'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
interface PageTransitionProps {
children: React.ReactNode;
}
export default function PageTransition({ children }: PageTransitionProps) {
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,
ease: [0.25, 0.46, 0.45, 0.94],
type: "spring",
stiffness: 100,
damping: 15
}
},
exit: {
opacity: 0,
...exitDirections[direction],
scale: 0.9,
rotateX: 15,
transition: {
duration: 0.3,
ease: "easeIn"
}
}
};
};
const getLoadingVariants = () => ({
initial: {
opacity: 0,
scale: 0.8,
y: 20
},
animate: {
opacity: 1,
scale: 1,
y: 0,
transition: {
duration: 0.3,
ease: "easeOut"
}
},
exit: {
opacity: 0,
scale: 0.8,
y: -20,
transition: {
duration: 0.2,
ease: "easeIn"
}
}
});
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, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}}
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>
</>
);
}