ui: 页面切换过渡动画 — AnimatePresence 淡入滑出效果

This commit is contained in:
2026-02-26 16:41:39 +08:00
parent 20f63c67cb
commit add9733bc9
2 changed files with 47 additions and 1 deletions
+2 -1
View File
@@ -3,6 +3,7 @@ import { Geist } from "next/font/google";
import "./globals.css";
import GlobalUserBadge from "@/components/GlobalUserBadge";
import ServiceWorkerRegistrar from "@/components/ServiceWorkerRegistrar";
import PageTransition from "@/components/PageTransition";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -40,7 +41,7 @@ export default function RootLayout({
<body className={`${geistSans.variable} font-sans antialiased`}>
<ServiceWorkerRegistrar />
<GlobalUserBadge />
{children}
<PageTransition>{children}</PageTransition>
</body>
</html>
);
+45
View File
@@ -0,0 +1,45 @@
"use client";
import { useContext, useRef, type PropsWithChildren } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
/**
* Preserves the previous route's React context during the exit animation
* so the old page doesn't break while fading out.
*/
function FrozenRoute({ children }: PropsWithChildren) {
const ctx = useContext(LayoutRouterContext);
const frozen = useRef(ctx).current;
return (
<LayoutRouterContext.Provider value={frozen}>
{children}
</LayoutRouterContext.Provider>
);
}
const variants = {
enter: { opacity: 0, y: 12 },
center: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -12 },
};
export default function PageTransition({ children }: PropsWithChildren) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={pathname}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
>
<FrozenRoute>{children}</FrozenRoute>
</motion.div>
</AnimatePresence>
);
}