AwsLinker/app/components/Header.tsx
2025-09-16 17:19:58 +08:00

452 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { ChevronDown, Menu, X, Globe, Phone, MessageCircle, Send, Search } from 'lucide-react';
import Logo from './Logo';
import { contactConfig } from '../../lib/useContact';
import { homeTranslations } from '../../lib/home-translations';
interface HeaderProps {
language: string;
setLanguage: (lang: string) => void;
translations: any;
locale?: string;
}
interface NavigationItem {
path: string;
label: string;
hasSubmenu?: boolean;
submenu?: { path: string; label: string; description?: string; }[];
}
export default function Header({ language, setLanguage, translations, locale }: HeaderProps) {
const pathname = usePathname();
const router = useRouter();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
// 处理滚动效果
useEffect(() => {
// 确保在客户端执行
if (typeof window === 'undefined') return;
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
setMounted(true);
}, []);
// 获取当前locale
const currentLocale = useMemo(() => {
if (!mounted) return locale || 'zh-CN';
return locale || (pathname.startsWith('/en') ? 'en' : pathname.startsWith('/zh-TW') ? 'zh-TW' : 'zh-CN');
}, [mounted, locale, pathname]);
// 同步语言状态
useEffect(() => {
if (mounted && language !== currentLocale) {
setLanguage(currentLocale);
}
}, [mounted, currentLocale, language, setLanguage]);
// 获取当前翻译
const t = useMemo(() => {
return translations || homeTranslations[currentLocale] || homeTranslations['zh-CN'];
}, [translations, currentLocale]);
// 获取路径不含locale前缀
const pathWithoutLocale = useMemo(() => {
if (!mounted) return '/';
return pathname.replace(/^\/(en|zh-TW|zh-CN)/, '') || '/';
}, [mounted, pathname]);
// 导航项配置
const navigationItems: NavigationItem[] = useMemo(() => [
{ path: '/', label: t.nav?.home || '首页' },
{
path: '/products',
label: t.nav?.products || '产品与服务',
hasSubmenu: true,
submenu: [
{ path: '/products#cloud-server', label: '云服务器', description: '弹性可扩展的计算服务' },
{ path: '/products#storage', label: '存储服务', description: '安全可靠的数据存储' },
{ path: '/products#database', label: '数据库服务', description: '高性能托管数据库' },
{ path: '/products#network', label: '网络服务', description: '全球网络加速服务' }
]
},
{ path: '/news', label: t.nav?.news || '新闻资讯' },
{ path: '/support', label: t.nav?.support || '客户支持' },
{ path: '/about', label: t.nav?.about || '关于我们' }
], [t.nav]);
// 语言选项
const languageOptions = useMemo(() => [
{ code: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
{ code: 'zh-TW', label: '繁體中文', flag: '🇹🇼' },
{ code: 'en', label: 'English', flag: '🇺🇸' }
], []);
// 检查是否为活跃路径
const isActive = useCallback((path: string) => {
if (path === '/') return pathWithoutLocale === '/';
return pathWithoutLocale.startsWith(path);
}, [pathWithoutLocale]);
// 生成本地化路径
const getLocalizedPath = useCallback((path: string, targetLocale: string = currentLocale) => {
return `/${targetLocale}${path}`;
}, [currentLocale]);
// 处理语言切换
const handleLanguageChange = useCallback((newLanguage: string) => {
if (!mounted) return;
setLanguage(newLanguage);
setIsLanguageDropdownOpen(false);
const newPath = getLocalizedPath(pathWithoutLocale, newLanguage);
router.push(newPath);
}, [mounted, setLanguage, pathWithoutLocale, router, getLocalizedPath]);
// 切换移动菜单
const toggleMobileMenu = useCallback(() => {
setIsMobileMenuOpen(prev => !prev);
}, []);
// 处理子菜单
const handleSubmenuEnter = useCallback((path: string) => {
setActiveSubmenu(path);
}, []);
const handleSubmenuLeave = useCallback(() => {
setActiveSubmenu(null);
}, []);
// 关闭所有菜单
const closeAllMenus = useCallback(() => {
setIsMobileMenuOpen(false);
setIsLanguageDropdownOpen(false);
setActiveSubmenu(null);
}, []);
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;
if (!target.closest('.header-menu')) {
closeAllMenus();
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [closeAllMenus]);
// 渲染桌面导航项
const renderDesktopNavItem = (item: NavigationItem) => {
const isActiveItem = isActive(item.path);
return (
<div
key={item.path}
className="relative group"
onMouseEnter={() => item.hasSubmenu && handleSubmenuEnter(item.path)}
onMouseLeave={() => item.hasSubmenu && handleSubmenuLeave()}
>
<Link
href={getLocalizedPath(item.path)}
className={`relative px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 flex items-center gap-1 ${
isActiveItem
? 'text-blue-600 bg-blue-50'
: 'text-gray-700 hover:text-blue-600 hover:bg-gray-50'
}`}
>
{item.label}
{item.hasSubmenu && (
<ChevronDown size={14} className={`transition-transform duration-200 ${
activeSubmenu === item.path ? 'rotate-180' : ''
}`} />
)}
{isActiveItem && (
<span className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-blue-600 rounded-full"></span>
)}
</Link>
{/* 子菜单 */}
{item.hasSubmenu && item.submenu && (
<div className={`absolute top-full left-0 mt-1 w-72 bg-white rounded-lg shadow-lg border border-gray-200 py-2 transition-all duration-200 ${
activeSubmenu === item.path
? 'opacity-100 visible translate-y-0'
: 'opacity-0 invisible -translate-y-2'
}`}>
{item.submenu.map((subItem) => (
<Link
key={subItem.path}
href={getLocalizedPath(subItem.path)}
className="block px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors"
onClick={closeAllMenus}
>
<div className="font-medium">{subItem.label}</div>
{subItem.description && (
<div className="text-xs text-gray-500 mt-1">{subItem.description}</div>
)}
</Link>
))}
</div>
)}
</div>
);
};
// 渲染移动端导航项
const renderMobileNavItem = (item: NavigationItem) => {
const isActiveItem = isActive(item.path);
return (
<div key={item.path} className="space-y-1">
<Link
href={getLocalizedPath(item.path)}
className={`flex items-center justify-between px-4 py-3 rounded-lg text-base font-medium transition-colors ${
isActiveItem
? 'text-blue-600 bg-blue-50'
: 'text-gray-700 hover:text-blue-600 hover:bg-gray-50'
}`}
onClick={closeAllMenus}
>
{item.label}
{isActiveItem && <span className="w-2 h-2 bg-blue-600 rounded-full"></span>}
</Link>
{/* 移动端子菜单 */}
{item.hasSubmenu && item.submenu && (
<div className="pl-4 space-y-1">
{item.submenu.map((subItem) => (
<Link
key={subItem.path}
href={getLocalizedPath(subItem.path)}
className="block px-4 py-2 text-sm text-gray-600 hover:text-blue-600 hover:bg-gray-50 rounded-md transition-colors"
onClick={closeAllMenus}
>
{subItem.label}
</Link>
))}
</div>
)}
</div>
);
};
return (
<header className={`sticky top-0 z-50 transition-all duration-300 ${
isScrolled
? 'bg-white/95 backdrop-blur-md shadow-md'
: 'bg-white shadow-sm'
}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link
href={getLocalizedPath('/')}
className="flex items-center space-x-2 hover:opacity-80 transition-opacity"
>
<Logo size="sm" variant="dark" />
</Link>
{/* 桌面端导航 */}
<nav className="hidden lg:flex items-center space-x-1">
{navigationItems.map(renderDesktopNavItem)}
</nav>
{/* 桌面端右侧操作区 */}
<div className="hidden lg:flex items-center space-x-4">
{/* 联系方式 */}
<div className="flex items-center space-x-3 text-sm border-r border-gray-200 pr-4">
<a
href={contactConfig.telegram}
className="flex items-center space-x-1 text-gray-600 hover:text-blue-500 transition-colors"
title="Telegram"
target="_blank"
rel="noopener noreferrer"
>
<Send size={16} />
<span className="hidden xl:inline">Telegram</span>
</a>
<a
href={contactConfig.whatsapp}
className="flex items-center space-x-1 text-gray-600 hover:text-green-600 transition-colors"
title="WhatsApp"
target="_blank"
rel="noopener noreferrer"
>
<MessageCircle size={16} />
<span className="hidden xl:inline">WhatsApp</span>
</a>
<a
href={contactConfig.phoneHref}
className="flex items-center space-x-1 text-gray-600 hover:text-blue-600 transition-colors"
title="电话联系"
>
<Phone size={16} />
<span className="hidden xl:inline">{contactConfig.phone}</span>
</a>
</div>
{/* 语言选择器 */}
<div className="relative header-menu">
<button
onClick={() => setIsLanguageDropdownOpen(!isLanguageDropdownOpen)}
className="flex items-center space-x-1 px-3 py-2 text-sm text-gray-600 hover:text-blue-600 rounded-md hover:bg-gray-50 transition-colors"
aria-label="选择语言"
title="选择语言"
>
<Globe size={16} />
<span>{languageOptions.find(opt => opt.code === currentLocale)?.flag}</span>
<ChevronDown size={14} className={`transition-transform duration-200 ${
isLanguageDropdownOpen ? 'rotate-180' : ''
}`} />
</button>
{isLanguageDropdownOpen && (
<div className="absolute right-0 top-full mt-1 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
{languageOptions.map((option) => (
<button
key={option.code}
onClick={() => handleLanguageChange(option.code)}
className={`w-full flex items-center space-x-3 px-4 py-2 text-sm text-left hover:bg-gray-50 transition-colors ${
currentLocale === option.code ? 'text-blue-600 bg-blue-50' : 'text-gray-700'
}`}
>
<span>{option.flag}</span>
<span>{option.label}</span>
{currentLocale === option.code && (
<span className="ml-auto w-2 h-2 bg-blue-600 rounded-full"></span>
)}
</button>
))}
</div>
)}
</div>
{/* 联系我们按钮 */}
<Link
href={getLocalizedPath('/contact')}
className="bg-gradient-to-r from-blue-600 to-blue-700 text-white px-4 py-2 rounded-md hover:from-blue-700 hover:to-blue-800 transition-all duration-200 shadow-sm hover:shadow-md transform hover:-translate-y-0.5"
>
{t.nav?.contact || '联系我们'}
</Link>
</div>
{/* 移动端菜单按钮 */}
<div className="lg:hidden flex items-center space-x-3">
{/* 移动端语言选择 */}
<div className="relative header-menu">
<button
onClick={() => setIsLanguageDropdownOpen(!isLanguageDropdownOpen)}
className="p-2 text-gray-600 hover:text-blue-600 rounded-md hover:bg-gray-50 transition-colors"
aria-label="选择语言"
title="选择语言"
>
<Globe size={20} />
</button>
{isLanguageDropdownOpen && (
<div className="absolute right-0 top-full mt-1 w-40 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
{languageOptions.map((option) => (
<button
key={option.code}
onClick={() => handleLanguageChange(option.code)}
className={`w-full flex items-center space-x-2 px-3 py-2 text-sm text-left hover:bg-gray-50 transition-colors ${
currentLocale === option.code ? 'text-blue-600 bg-blue-50' : 'text-gray-700'
}`}
>
<span>{option.flag}</span>
<span className="text-xs">{option.code.toUpperCase()}</span>
</button>
))}
</div>
)}
</div>
{/* 汉堡菜单按钮 */}
<button
onClick={toggleMobileMenu}
className="p-2 text-gray-600 hover:text-blue-600 rounded-md hover:bg-gray-50 transition-colors"
aria-label="切换菜单"
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
{/* 移动端菜单 */}
<div className={`lg:hidden transition-all duration-300 ease-in-out ${
isMobileMenuOpen
? 'max-h-screen opacity-100'
: 'max-h-0 opacity-0 overflow-hidden'
}`}>
<div className="pb-4 pt-2 border-t border-gray-200">
{/* 移动端导航项 */}
<div className="space-y-1 mb-6">
{navigationItems.map(renderMobileNavItem)}
</div>
{/* 移动端联系我们按钮 */}
<div className="px-4">
<Link
href={getLocalizedPath('/contact')}
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 text-white text-center px-4 py-3 rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all duration-200 font-medium block"
onClick={closeAllMenus}
>
{t.nav?.contact || '联系我们'}
</Link>
</div>
{/* 移动端联系方式 */}
<div className="mt-6 pt-4 border-t border-gray-200 px-4">
<div className="flex justify-center space-x-6">
<a
href={contactConfig.telegram}
className="flex items-center space-x-2 text-blue-500 hover:text-blue-600 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<Send size={20} />
<span className="text-sm">Telegram</span>
</a>
<a
href={contactConfig.whatsapp}
className="flex items-center space-x-2 text-green-600 hover:text-green-700 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<MessageCircle size={20} />
<span className="text-sm">WhatsApp</span>
</a>
<a
href={contactConfig.phoneHref}
className="flex items-center space-x-2 text-blue-600 hover:text-blue-700 transition-colors"
>
<Phone size={20} />
<span className="text-sm"></span>
</a>
</div>
</div>
</div>
</div>
</div>
</header>
);
}