a003f1192e
- 字体加载优化: 添加 font-display: block 策略,创建 useFontLoading hook - 色彩对比度: 调整 text-muted 和 text-tertiary 颜色值确保 WCAG AA 合规 - 滚动进度条: 新增 ScrollProgress 组件,支持 reduced motion - 表单自动保存: 新增 useFormAutosave hook,防止用户数据丢失 - 返回顶部按钮: 新增 BackToTop 组件,提升长页面导航体验 - 图片懒加载: 优化 OptimizedImage 组件,添加 blur placeholder 和加载动画 所有新组件均包含完整测试,1450+ 测试通过
53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { ArrowUp } from 'lucide-react';
|
|
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
|
|
|
export function BackToTop() {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
// 当滚动超过 500px 时显示按钮
|
|
setIsVisible(window.scrollY > 500);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
const scrollToTop = () => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: shouldReduceMotion ? 'auto' : 'smooth',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isVisible && (
|
|
<motion.button
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.8 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={shouldReduceMotion ? {} : { opacity: 0, y: 20, scale: 0.8 }}
|
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
onClick={scrollToTop}
|
|
className="fixed bottom-8 right-8 z-50 p-3 bg-[#C41E3A] text-white rounded-full shadow-lg hover:bg-[#A01830] hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-offset-2"
|
|
aria-label="返回顶部"
|
|
title="返回顶部"
|
|
style={{
|
|
boxShadow: '0 4px 14px rgba(196, 30, 58, 0.4)',
|
|
}}
|
|
whileHover={shouldReduceMotion ? {} : { scale: 1.1 }}
|
|
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
|
|
>
|
|
<ArrowUp className="w-6 h-6" />
|
|
</motion.button>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|