forked from CarrotSkin/carrotskin
Initial commit: CarrotSkin project setup
This commit is contained in:
177
src/components/PageTransition.tsx
Normal file
177
src/components/PageTransition.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user