Files
InfoGenie/InfoGenie-frontend/src/components/Header.js
2025-12-13 20:53:50 +08:00

451 lines
11 KiB
JavaScript
Executable File

import React, { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { FiUser, FiMenu, FiX, FiLogOut, FiInfo } from 'react-icons/fi';
import { useUser } from '../contexts/UserContext';
const HeaderContainer = styled.header`
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
color: white;
padding: 12px 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 1000;
`;
const HeaderContent = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
`;
const Logo = styled(Link)`
display: flex;
align-items: center;
font-size: 20px;
font-weight: bold;
text-decoration: none;
color: white;
.logo-icon {
margin-right: 8px;
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 50%;
}
@media (max-width: 768px) {
font-size: 18px;
.logo-icon {
width: 30px;
height: 30px;
}
}
`;
const Nav = styled.nav`
display: flex;
align-items: center;
gap: 24px;
@media (max-width: 768px) {
display: none;
}
`;
const NavLink = styled(Link).withConfig({
shouldForwardProp: (prop) => prop !== 'isActive'
})`
color: ${props => props.isActive ? 'white' : 'rgba(255, 255, 255, 0.9)'};
text-decoration: none;
padding: 10px 18px;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
font-weight: ${props => props.isActive ? '600' : '500'};
background: ${props => props.isActive ? 'rgba(255, 255, 255, 0.2)' : 'transparent'};
box-shadow: ${props => props.isActive ? '0 4px 12px rgba(0, 0, 0, 0.15)' : 'none'};
transform: ${props => props.isActive ? 'translateY(-1px)' : 'translateY(0)'};
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100%);
border-radius: 12px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
&:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
&::before {
opacity: 1;
}
}
&:active {
transform: translateY(0);
transition: transform 0.1s ease;
}
`;
const UserSection = styled.div`
display: flex;
align-items: center;
gap: 12px;
`;
const UserButton = styled.button`
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
@media (max-width: 768px) {
padding: 8px 12px;
.user-text {
display: none;
}
}
`;
const UserAvatar = styled.img`
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
object-fit: cover;
`;
const UserInfo = styled.div`
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
padding: 6px 12px;
border-radius: 6px;
@media (max-width: 768px) {
.user-name {
display: none;
}
}
`;
const MobileMenuButton = styled.button`
display: none;
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
@media (max-width: 768px) {
display: block;
}
`;
const MobileMenu = styled.div.withConfig({
shouldForwardProp: (prop) => prop !== 'isOpen'
})`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
display: ${props => props.isOpen ? 'block' : 'none'};
@media (min-width: 769px) {
display: none;
}
`;
const MobileMenuContent = styled.div.withConfig({
shouldForwardProp: (prop) => prop !== 'isOpen'
})`
position: absolute;
top: 0;
right: 0;
width: 280px;
height: 100vh;
background: linear-gradient(135deg,
rgba(255, 240, 245, 0.95) 0%, /* 淡粉红色 */
rgba(255, 253, 240, 0.95) 35%, /* 淡黄色 */
rgba(240, 255, 240, 0.95) 70%, /* 淡绿色 */
rgba(248, 250, 252, 0.95) 100% /* 接近白色 */
);
backdrop-filter: blur(10px);
transform: translateX(${props => props.isOpen ? '0' : '100%'});
transition: transform 0.3s ease;
padding: 20px;
overflow-y: auto;
`;
const MobileMenuHeader = styled.div`
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
`;
const MobileMenuTitle = styled.h3`
color: #1f2937;
margin: 0;
`;
const CloseButton = styled.button`
background: none;
border: none;
font-size: 24px;
color: #6b7280;
cursor: pointer;
`;
const MobileNavLink = styled(Link).withConfig({
shouldForwardProp: (prop) => prop !== 'isActive'
})`
display: block;
color: ${props => props.isActive ? '#4ade80' : '#374151'};
text-decoration: none;
padding: 16px 20px;
margin: 4px 0;
border-radius: 12px;
border-bottom: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
font-weight: ${props => props.isActive ? '600' : '500'};
background: ${props => props.isActive ? 'rgba(74, 222, 128, 0.1)' : 'transparent'};
transform: ${props => props.isActive ? 'translateX(8px)' : 'translateX(0)'};
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: ${props => props.isActive ? '4px' : '0'};
height: 60%;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
border-radius: 0 2px 2px 0;
transition: width 0.3s ease;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(74, 222, 128, 0.05) 0%, rgba(34, 197, 94, 0.05) 100%);
border-radius: 12px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
&:hover {
color: #4ade80;
background: rgba(74, 222, 128, 0.08);
transform: translateX(12px);
box-shadow: 0 4px 12px rgba(74, 222, 128, 0.15);
&::after {
opacity: 1;
}
&::before {
width: 4px;
}
}
&:active {
transform: translateX(6px);
transition: transform 0.1s ease;
}
`;
const Header = () => {
const { user, isLoggedIn, logout, getQQAvatar } = useUser();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const isActive = (path) => {
return location.pathname.startsWith(path);
};
const handleLogout = async () => {
await logout();
setIsMenuOpen(false);
navigate('/');
};
const handleMenuToggle = () => {
setIsMenuOpen(!isMenuOpen);
};
const handleMenuClose = () => {
setIsMenuOpen(false);
};
return (
<>
<HeaderContainer>
<HeaderContent>
<Logo to="/">
<img className="logo-icon" src="/assets/logo.png" alt="InfoGenie Logo" />
万象口袋
</Logo>
<Nav>
<NavLink to="/60sapi" isActive={isActive('/60sapi')}>聚合应用</NavLink>
<NavLink to="/smallgame" isActive={isActive('/smallgame')}>休闲游戏</NavLink>
<NavLink to="/aimodel" isActive={isActive('/aimodel')}>AI工具</NavLink>
<NavLink to="/profile" isActive={isActive('/profile')}>个人中心</NavLink>
</Nav>
<UserSection>
{isLoggedIn && user ? (
<>
<UserInfo>
{getQQAvatar(user.email) ? (
<UserAvatar
src={getQQAvatar(user.email)}
alt="QQ头像"
onError={(e) => {
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'inline';
}}
/>
) : null}
<FiUser style={{ display: getQQAvatar(user.email) ? 'none' : 'inline' }} />
<span className="user-name">{user.username || user.email}</span>
</UserInfo>
<UserButton onClick={handleLogout}>
<FiLogOut size={16} />
<span className="user-text">退出</span>
</UserButton>
</>
) : (
<UserButton as={Link} to="/login">
<FiUser />
<span className="user-text">登录</span>
</UserButton>
)}
<MobileMenuButton onClick={handleMenuToggle}>
<FiMenu />
</MobileMenuButton>
</UserSection>
</HeaderContent>
</HeaderContainer>
<MobileMenu isOpen={isMenuOpen} onClick={handleMenuClose}>
<MobileMenuContent isOpen={isMenuOpen} onClick={(e) => e.stopPropagation()}>
<MobileMenuHeader>
<MobileMenuTitle>菜单</MobileMenuTitle>
<CloseButton onClick={handleMenuClose}>
<FiX />
</CloseButton>
</MobileMenuHeader>
<MobileNavLink to="/" onClick={handleMenuClose} isActive={location.pathname === '/'}>
首页
</MobileNavLink>
<MobileNavLink to="/60sapi" onClick={handleMenuClose} isActive={isActive('/60sapi')}>
聚合应用
</MobileNavLink>
<MobileNavLink to="/smallgame" onClick={handleMenuClose} isActive={isActive('/smallgame')}>
休闲游戏
</MobileNavLink>
<MobileNavLink to="/aimodel" onClick={handleMenuClose} isActive={isActive('/aimodel')}>
AI工具
</MobileNavLink>
<MobileNavLink to="/about" onClick={handleMenuClose} isActive={isActive('/about')}>
关于
</MobileNavLink>
<MobileNavLink to="/profile" onClick={handleMenuClose} isActive={isActive('/profile')}>
个人中心
</MobileNavLink>
{isLoggedIn && user ? (
<>
<div style={{
padding: '12px 0',
borderBottom: '1px solid #f3f4f6',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
{getQQAvatar(user.email) ? (
<UserAvatar
src={getQQAvatar(user.email)}
alt="QQ头像"
style={{ width: '32px', height: '32px' }}
/>
) : (
<FiUser size={24} color="#666" />
)}
<span style={{ color: '#374151', fontWeight: '500' }}>{user.username || user.email}</span>
</div>
<MobileNavLink as="button"
style={{
background: 'none',
border: 'none',
width: '100%',
textAlign: 'left',
color: '#ef4444'
}}
onClick={() => {
handleLogout();
handleMenuClose();
}}
>
🚪 退出登录
</MobileNavLink>
</>
) : (
<MobileNavLink to="/login" onClick={handleMenuClose}>
登录注册
</MobileNavLink>
)}
</MobileMenuContent>
</MobileMenu>
</>
);
};
export default Header;