forked from CarrotSkin/carrotskin
178 lines
4.7 KiB
TypeScript
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>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|