65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
"use client";
|
|||
|
|
|
||
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||
|
|
import { ArrowUp } from "lucide-react";
|
||
|
|
|
||
|
|
export function BackToTopButton() {
|
||
|
|
const [visible, setVisible] = useState(false);
|
||
|
|
const prefersReducedMotion = useReducedMotion();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let frame = 0;
|
||
|
|
|
||
|
|
const updateVisibility = () => {
|
||
|
|
frame = 0;
|
||
|
|
setVisible(window.scrollY > 520);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleScroll = () => {
|
||
|
|
if (frame) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
frame = window.requestAnimationFrame(updateVisibility);
|
||
|
|
};
|
||
|
|
|
||
|
|
updateVisibility();
|
||
|
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener("scroll", handleScroll);
|
||
|
|
if (frame) {
|
||
|
|
window.cancelAnimationFrame(frame);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const scrollToTop = () => {
|
||
|
|
window.scrollTo({
|
||
|
|
top: 0,
|
||
|
|
behavior: prefersReducedMotion ? "auto" : "smooth",
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AnimatePresence>
|
||
|
|
{visible ? (
|
||
|
|
<motion.button
|
||
|
|
type="button"
|
||
|
|
aria-label="回到顶部"
|
||
|
|
title="回到顶部"
|
||
|
|
className="fixed bottom-6 right-5 z-50 inline-flex h-12 w-12 items-center justify-center rounded-full border border-cyan-300/35 bg-[#07111f]/95 text-cyan-100 shadow-[0_0_28px_rgba(34,211,238,0.24)] outline-none transition hover:-translate-y-0.5 hover:border-fuchsia-300/70 hover:text-white hover:shadow-[0_0_34px_rgba(217,70,239,0.25)] focus-visible:ring-2 focus-visible:ring-cyan-300 sm:bottom-8 sm:right-8"
|
||
|
|
initial={{ opacity: 0, scale: 0.82, y: 12 }}
|
||
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
|
|
exit={{ opacity: 0, scale: 0.82, y: 12 }}
|
||
|
|
transition={{ duration: 0.18, ease: "easeOut" }}
|
||
|
|
onClick={scrollToTop}
|
||
|
|
>
|
||
|
|
<ArrowUp className="h-5 w-5" />
|
||
|
|
</motion.button>
|
||
|
|
) : null}
|
||
|
|
</AnimatePresence>
|
||
|
|
);
|
||
|
|
}
|