2025-12-04 20:05:13 +08:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
|
|
|
import Link from 'next/link';
|
|
|
|
|
|
import { useRouter, usePathname } from 'next/navigation';
|
|
|
|
|
|
import { Bars3Icon, XMarkIcon, UserCircleIcon } from '@heroicons/react/24/outline';
|
2025-12-05 20:07:50 +08:00
|
|
|
|
import { motion, AnimatePresence, useScroll, useTransform, useSpring } from 'framer-motion';
|
2025-12-04 20:05:13 +08:00
|
|
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
|
|
|
|
|
|
|
|
|
|
export default function Navbar() {
|
|
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
|
|
const [isHidden, setIsHidden] = useState(false);
|
|
|
|
|
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
|
|
|
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
|
|
|
|
|
const [navbarHeight, setNavbarHeight] = useState(0);
|
2025-12-05 20:07:50 +08:00
|
|
|
|
const [scrollProgress, setScrollProgress] = useState(0);
|
2025-12-04 20:05:13 +08:00
|
|
|
|
const navbarRef = useRef<HTMLElement>(null);
|
2026-07-09 17:14:36 +08:00
|
|
|
|
const { user, isAuthenticated, isLoading: isAuthLoading, logout } = useAuth();
|
2025-12-04 20:05:13 +08:00
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const pathname = usePathname();
|
2025-12-05 20:07:50 +08:00
|
|
|
|
const { scrollY } = useScroll();
|
|
|
|
|
|
|
|
|
|
|
|
// 弹簧动画效果
|
|
|
|
|
|
const springConfig = { stiffness: 300, damping: 30 };
|
|
|
|
|
|
const navbarY = useSpring(useTransform(scrollY, [0, 100], [0, -100]), springConfig);
|
|
|
|
|
|
const navbarOpacity = useSpring(useTransform(scrollY, [0, 50], [1, 0.95]), springConfig);
|
|
|
|
|
|
const scrollProgressSpring = useSpring(scrollProgress, springConfig);
|
2025-12-04 20:05:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 在auth页面隐藏navbar
|
|
|
|
|
|
const isAuthPage = pathname === '/auth';
|
|
|
|
|
|
|
|
|
|
|
|
// 检测navbar高度并设置CSS自定义属性
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const updateHeight = () => {
|
|
|
|
|
|
if (navbarRef.current) {
|
|
|
|
|
|
const height = navbarRef.current.offsetHeight;
|
|
|
|
|
|
setNavbarHeight(height);
|
|
|
|
|
|
document.documentElement.style.setProperty('--navbar-height', `${height}px`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
updateHeight();
|
|
|
|
|
|
window.addEventListener('resize', updateHeight);
|
|
|
|
|
|
return () => window.removeEventListener('resize', updateHeight);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
|
// 滚动进度计算
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
const scrollTop = window.scrollY;
|
|
|
|
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
|
|
|
|
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
|
|
|
|
|
setScrollProgress(progress);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
|
|
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-12-04 20:05:13 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let lastScrollY = 0;
|
|
|
|
|
|
let ticking = false;
|
|
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
if (!ticking) {
|
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
|
const currentScrollY = window.scrollY;
|
|
|
|
|
|
|
|
|
|
|
|
// 检测是否滚动到顶部
|
|
|
|
|
|
setIsScrolled(currentScrollY > 20);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示返回顶部按钮(滚动超过300px)
|
|
|
|
|
|
setShowScrollTop(currentScrollY > 300);
|
|
|
|
|
|
|
|
|
|
|
|
// 更敏感的隐藏逻辑:只要往下滚动就隐藏,不管滚动多少
|
|
|
|
|
|
if (!isAuthPage && currentScrollY > lastScrollY && currentScrollY > 10) {
|
|
|
|
|
|
setIsHidden(true);
|
|
|
|
|
|
} else if (currentScrollY < lastScrollY) { // 往上滚动就显示
|
|
|
|
|
|
setIsHidden(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastScrollY = currentScrollY;
|
|
|
|
|
|
ticking = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
ticking = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!isAuthPage) {
|
|
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
|
|
|
|
}, [isAuthPage]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleLogout = () => {
|
|
|
|
|
|
logout();
|
|
|
|
|
|
router.push('/');
|
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleLinkClick = () => {
|
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 在auth页面不渲染navbar
|
|
|
|
|
|
if (isAuthPage) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scrollToTop = () => {
|
|
|
|
|
|
window.scrollTo({
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
|
const navItems = [
|
|
|
|
|
|
{ href: '/', label: '首页', icon: null },
|
|
|
|
|
|
{ href: '/skins', label: '皮肤库', icon: null },
|
|
|
|
|
|
];
|
2025-12-04 20:05:13 +08:00
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<motion.nav
|
|
|
|
|
|
ref={navbarRef}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
y: isHidden ? navbarY : 0,
|
2026-01-09 17:44:21 +08:00
|
|
|
|
opacity: navbarOpacity,
|
|
|
|
|
|
willChange: 'transform, opacity'
|
2025-12-05 20:07:50 +08:00
|
|
|
|
}}
|
|
|
|
|
|
initial={{ y: 0 }}
|
|
|
|
|
|
animate={{ y: isHidden ? -100 : 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
|
|
|
|
|
className="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg border-b border-gray-200/50 dark:border-gray-700/50"
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 滚动进度条 */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-orange-400 to-amber-500"
|
|
|
|
|
|
style={{ width: `${scrollProgress}%` }}
|
|
|
|
|
|
initial={{ width: 0 }}
|
|
|
|
|
|
animate={{ width: `${scrollProgress}%` }}
|
|
|
|
|
|
transition={{ duration: 0.1 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className={`
|
|
|
|
|
|
max-w-7xl mx-auto px-4 sm:px-6 lg:px-8
|
|
|
|
|
|
${isScrolled ? 'py-3' : 'py-4'}
|
|
|
|
|
|
transition-all duration-300
|
|
|
|
|
|
`}>
|
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
|
{/* Logo */}
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="flex items-center"
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
|
|
whileTap={{ scale: 0.98 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link href="/" className="flex items-center space-x-3 group">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="w-10 h-10 bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 rounded-xl flex items-center justify-center shadow-lg relative overflow-hidden"
|
|
|
|
|
|
whileHover={{ rotate: 5, scale: 1.05 }}
|
|
|
|
|
|
transition={{ type: 'spring', stiffness: 300 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-white font-bold text-lg relative z-10">C</span>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent"
|
|
|
|
|
|
initial={{ x: '-100%', y: '-100%' }}
|
|
|
|
|
|
whileHover={{ x: '100%', y: '100%' }}
|
|
|
|
|
|
transition={{ duration: 0.6 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</motion.div>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
<motion.span
|
2025-12-05 20:07:50 +08:00
|
|
|
|
className="text-2xl font-black bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent"
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
CarrotSkin
|
|
|
|
|
|
</motion.span>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Desktop Navigation */}
|
|
|
|
|
|
<div className="hidden md:flex items-center space-x-6">
|
|
|
|
|
|
{navItems.map((item, index) => (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
key={item.href}
|
|
|
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ delay: index * 0.1 }}
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
whileTap={{ scale: 0.98 }}
|
|
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<Link
|
|
|
|
|
|
href={item.href}
|
|
|
|
|
|
className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-all duration-200 font-medium relative group px-3 py-2 rounded-lg hover:bg-orange-500/10 dark:hover:bg-orange-400/10"
|
2025-12-04 20:05:13 +08:00
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{item.label}
|
|
|
|
|
|
<motion.span
|
|
|
|
|
|
className="absolute -bottom-0.5 left-3 right-3 h-0.5 bg-gradient-to-r from-orange-400 to-orange-600"
|
|
|
|
|
|
initial={{ scaleX: 0 }}
|
|
|
|
|
|
whileHover={{ scaleX: 1 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
/>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{pathname === item.href && (
|
|
|
|
|
|
<motion.span
|
|
|
|
|
|
className="absolute -bottom-0.5 left-3 right-3 h-0.5 bg-gradient-to-r from-orange-400 to-orange-600"
|
|
|
|
|
|
initial={{ scaleX: 0 }}
|
|
|
|
|
|
animate={{ scaleX: 1 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
))}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{/* 用户头像框 - 增强的微交互 */}
|
2026-07-09 17:14:36 +08:00
|
|
|
|
{isAuthLoading ? (
|
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
|
<div className="w-9 h-9 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse" />
|
|
|
|
|
|
<div className="w-12 h-4 rounded bg-gray-200 dark:bg-gray-700 animate-pulse" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : isAuthenticated ? (
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
whileHover={{ scale: 1.05 }}
|
|
|
|
|
|
whileTap={{ scale: 0.95 }}
|
|
|
|
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/profile"
|
2025-12-05 20:07:50 +08:00
|
|
|
|
className="flex items-center space-x-3 group"
|
2025-12-04 20:05:13 +08:00
|
|
|
|
onClick={handleLinkClick}
|
|
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{user?.avatar ? (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="relative"
|
|
|
|
|
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
|
|
|
|
>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
<img
|
|
|
|
|
|
src={user.avatar}
|
|
|
|
|
|
alt={user.username}
|
2025-12-05 20:07:50 +08:00
|
|
|
|
className="w-9 h-9 rounded-full border-2 border-orange-500/30 group-hover:border-orange-500 transition-all duration-200 shadow-md"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
whileHover={{ opacity: 1 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute -inset-1 rounded-full border-2 border-orange-400/50"
|
|
|
|
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
|
|
|
|
whileHover={{ scale: 1.2, opacity: 1 }}
|
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="relative"
|
|
|
|
|
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<UserCircleIcon className="w-9 h-9 text-gray-400 group-hover:text-orange-500 transition-all duration-200" />
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
whileHover={{ opacity: 1 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
/>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<motion.span
|
|
|
|
|
|
className="text-gray-700 dark:text-gray-300 group-hover:text-orange-500 dark:group-hover:text-orange-400 transition-all duration-200 font-medium"
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{user?.username}
|
|
|
|
|
|
</motion.span>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<motion.button
|
|
|
|
|
|
onClick={handleLogout}
|
|
|
|
|
|
className="relative overflow-hidden border-2 border-orange-500 text-orange-500 hover:text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 group"
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
|
|
whileTap={{ scale: 0.98 }}
|
|
|
|
|
|
initial={{ opacity: 0, x: 10 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
2025-12-05 20:07:50 +08:00
|
|
|
|
transition={{ delay: 0.3 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<motion.span
|
|
|
|
|
|
className="absolute inset-0 w-0 bg-gradient-to-r from-orange-400 to-orange-600 transition-all duration-300 group-hover:w-full"
|
|
|
|
|
|
initial={{ width: 0 }}
|
|
|
|
|
|
whileHover={{ width: '100%' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="relative z-10">退出登录</span>
|
|
|
|
|
|
</motion.button>
|
|
|
|
|
|
</div>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
) : (
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<motion.div
|
|
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
|
|
whileTap={{ scale: 0.98 }}
|
|
|
|
|
|
initial={{ opacity: 0, x: 10 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/auth"
|
|
|
|
|
|
className="flex items-center space-x-2 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-all duration-200 group"
|
2025-12-04 20:05:13 +08:00
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<motion.div
|
|
|
|
|
|
className="relative"
|
|
|
|
|
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
<UserCircleIcon className="w-7 h-7 text-gray-400 group-hover:text-orange-500 transition-all duration-200" />
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
className="absolute inset-0 rounded-full bg-gradient-to-br from-orange-400/20 to-orange-600/20"
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
whileHover={{ opacity: 1 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
<span className="font-medium">登录</span>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Mobile menu button */}
|
|
|
|
|
|
<div className="md:hidden flex items-center">
|
|
|
|
|
|
<motion.button
|
|
|
|
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
|
|
|
|
className="text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 transition-colors duration-200 p-2"
|
|
|
|
|
|
whileHover={{ scale: 1.1 }}
|
|
|
|
|
|
whileTap={{ scale: 0.9 }}
|
|
|
|
|
|
animate={{ rotate: isOpen ? 180 : 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<AnimatePresence mode="wait">
|
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
key="close"
|
|
|
|
|
|
initial={{ rotate: -90, opacity: 0 }}
|
|
|
|
|
|
animate={{ rotate: 0, opacity: 1 }}
|
|
|
|
|
|
exit={{ rotate: 90, opacity: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<XMarkIcon className="w-6 h-6" />
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
key="open"
|
|
|
|
|
|
initial={{ rotate: 90, opacity: 0 }}
|
|
|
|
|
|
animate={{ rotate: 0, opacity: 1 }}
|
|
|
|
|
|
exit={{ rotate: -90, opacity: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Bars3Icon className="w-6 h-6" />
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</motion.button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Mobile Navigation */}
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{isOpen && (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, height: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
|
|
|
|
exit={{ opacity: 0, height: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
|
|
|
|
|
className="md:hidden bg-white/95 dark:bg-gray-900/95 backdrop-blur-md border-t border-gray-200/50 dark:border-gray-700/50 overflow-hidden"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="px-4 py-4 space-y-1">
|
|
|
|
|
|
{navItems.map((item, index) => (
|
2025-12-04 20:05:13 +08:00
|
|
|
|
<motion.div
|
2025-12-05 20:07:50 +08:00
|
|
|
|
key={item.href}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
2025-12-05 20:07:50 +08:00
|
|
|
|
transition={{ delay: index * 0.05 }}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Link
|
2025-12-05 20:07:50 +08:00
|
|
|
|
href={item.href}
|
|
|
|
|
|
className="block px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
|
2025-12-04 20:05:13 +08:00
|
|
|
|
onClick={handleLinkClick}
|
|
|
|
|
|
>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{item.label}
|
2025-12-04 20:05:13 +08:00
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{isAuthenticated ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/profile"
|
|
|
|
|
|
className="block px-4 py-3 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200"
|
|
|
|
|
|
onClick={handleLinkClick}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
|
{user?.avatar ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={user.avatar}
|
|
|
|
|
|
alt={user.username}
|
|
|
|
|
|
className="w-8 h-8 rounded-full border-2 border-orange-500/30"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<UserCircleIcon className="w-8 h-8 text-gray-400" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span className="text-gray-700 dark:text-gray-300 font-medium">{user?.username}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.25 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleLogout}
|
|
|
|
|
|
className="block w-full text-left px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
退出登录
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.2 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/auth"
|
|
|
|
|
|
className="block px-4 py-3 text-gray-700 dark:text-gray-300 hover:text-orange-500 dark:hover:text-orange-400 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 rounded-lg transition-all duration-200 font-medium"
|
|
|
|
|
|
onClick={handleLinkClick}
|
|
|
|
|
|
>
|
|
|
|
|
|
登录
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
|
|
|
animate={{ opacity: 1, x: 0 }}
|
|
|
|
|
|
transition={{ delay: 0.25 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/auth"
|
|
|
|
|
|
className="block px-4 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl font-medium text-center"
|
|
|
|
|
|
onClick={handleLinkClick}
|
|
|
|
|
|
>
|
|
|
|
|
|
注册
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</motion.nav>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 返回顶部按钮 */}
|
2026-01-13 20:32:13 +08:00
|
|
|
|
{/* <AnimatePresence>
|
2025-12-05 20:07:50 +08:00
|
|
|
|
{showScrollTop && (
|
|
|
|
|
|
<motion.button
|
|
|
|
|
|
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
|
|
|
|
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
|
|
|
|
|
whileHover={{ scale: 1.1, y: -2 }}
|
|
|
|
|
|
whileTap={{ scale: 0.9 }}
|
|
|
|
|
|
onClick={scrollToTop}
|
|
|
|
|
|
className="fixed bottom-8 right-8 z-40 bg-gradient-to-r from-orange-500 to-amber-500 text-white p-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
animate={{ y: [0, -3, 0] }}
|
|
|
|
|
|
transition={{ duration: 1.5, repeat: Infinity }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</motion.button>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
)}
|
2026-01-13 20:32:13 +08:00
|
|
|
|
</AnimatePresence> */}
|
2025-12-05 20:07:50 +08:00
|
|
|
|
</>
|
2025-12-04 20:05:13 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|