452 lines
21 KiB
TypeScript
Raw Permalink Normal View History

2025-09-16 17:19:58 +08:00
'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>
);
}