Header.jsx 13.3 KB
// src/components/Header.jsx
import React, { useState, useEffect, useRef } from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import LanguageSelector from './LanguageSelector';
import { useTranslation } from '../js/i18n';
import logo from '/assets/logo.png';

function Header() {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const user = JSON.parse(localStorage.getItem('user'));
  const [menuOpen, setMenuOpen] = useState(false);
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
  const menuRef = useRef(null);

  const toggleMenu = () => {
    setMenuOpen(!menuOpen);
  };

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (menuRef.current && !menuRef.current.contains(event.target)) {
        setMenuOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [menuRef]);

  const handleLogout = () => {
    localStorage.removeItem('user');
    navigate('/login');
    setMobileMenuOpen(false);
  };

  const toggleMobileMenu = () => {
    setMobileMenuOpen(!mobileMenuOpen);
  };

  const handleNavClick = () => {
    setMobileMenuOpen(false);
  };

  // 添加键盘导航支持
  const handleKeyDown = (e) => {
    if (e.key === 'Escape') {
      setMenuOpen(false);
      setMobileMenuOpen(false);
    }
  };

  // 添加焦点管理
  useEffect(() => {
    const handleFocusTrap = (e) => {
      if (!mobileMenuOpen) return;
      
      const mobileMenu = document.querySelector('[role="dialog"]');
      const focusableElements = mobileMenu.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      const firstElement = focusableElements[0];
      const lastElement = focusableElements[focusableElements.length - 1];

      if (e.key === 'Tab') {
        if (e.shiftKey && document.activeElement === firstElement) {
          e.preventDefault();
          lastElement.focus();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          e.preventDefault();
          firstElement.focus();
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    document.addEventListener('keydown', handleFocusTrap);
    
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      document.removeEventListener('keydown', handleFocusTrap);
    };
  }, [mobileMenuOpen]);

  // 处理滚动锁定
  useEffect(() => {
    if (mobileMenuOpen) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = '';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [mobileMenuOpen]);

  return (
    <header className="fixed top-0 left-0 right-0 z-50">
      <div className="absolute inset-0 bg-white/70 backdrop-blur-lg border-b border-gray-100"></div>
      
      <nav 
        className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
        role="navigation"
        aria-label={t('navigation.main')}
      >
        <div className="flex justify-between items-center h-20">
          <div className="flex items-center">
            <div className="flex-shrink-0 mr-8">
              <NavLink 
                to="/" 
                className="flex items-center space-x-3 group"
                aria-label={t('navigation.home')}
              >
                <img 
                  src={logo} 
                  alt={t('logo.alt')} 
                  className="w-10 h-10 object-contain transition transform group-hover:scale-105"
                  width="40"
                  height="40"
                  loading="eager"
                />
                <span className="text-xl font-semibold bg-gradient-to-r from-indigo-500/90 to-indigo-600/80 bg-clip-text text-transparent">
                  {t('title')}
                </span>
              </NavLink>
            </div>

            <div className="hidden md:flex items-center space-x-6">
              <NavLink 
                to="/dev-tools"
                className={({isActive}) => 
                  `px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
                    isActive 
                      ? 'text-indigo-500 border-indigo-500' 
                      : 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
                  }`
                }
              >
                {t('dev-tools.title')}
              </NavLink>
              <NavLink 
                to="/image-tools" 
                className={({isActive}) => 
                  `px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
                    isActive 
                      ? 'text-indigo-500 border-indigo-500' 
                      : 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
                  }`
                }
                onClick={handleNavClick}
              >
                {t('image-tools.title')}
              </NavLink>
              <NavLink 
                to="/ai-products" 
                className={({isActive}) => 
                  `px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
                    isActive 
                      ? 'text-indigo-500 border-indigo-500' 
                      : 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
                  }`
                }
                onClick={handleNavClick}
              >
                {t('ai-products.title')}
              </NavLink>
              <NavLink 
                to="/blog" 
                className={({isActive}) => 
                  `px-3 py-2 text-base font-medium transition-all duration-200 border-b-2 ${
                    isActive 
                      ? 'text-indigo-500 border-indigo-500' 
                      : 'text-gray-600 border-transparent hover:text-indigo-500 hover:border-indigo-500'
                  }`
                }
                onClick={handleNavClick}
              >
                {t('blog.title')}
              </NavLink>
            </div>
          </div>

          <div className="flex items-center space-x-4">
            <LanguageSelector />
            {user ? (
              <div className="relative" ref={menuRef}>
                <button
                  onClick={toggleMenu}
                  className="flex items-center space-x-3 focus:outline-none"
                >
                  <img
                    src={user.picture}
                    alt="User Avatar"
                    className="w-8 h-8 sm:w-10 sm:h-10 rounded-full ring-2 ring-offset-2 ring-indigo-500 transition transform hover:scale-105"
                  />
                </button>
                
                {menuOpen && (
                  <div className="absolute right-0 mt-2 w-48 rounded-lg shadow-lg bg-white ring-1 ring-black ring-opacity-5 py-1 focus:outline-none">
                    <button
                      onClick={handleLogout}
                      className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-colors duration-150"
                    >
                      {t('logout')}
                    </button>
                  </div>
                )}
              </div>
            ) : (
              <NavLink
                to="/login"
                className="hidden sm:inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md"
              >
                {t('login')}
              </NavLink>
            )}
          </div>

          <button 
            className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors duration-200"
            onClick={toggleMobileMenu}
            aria-expanded={mobileMenuOpen}
            aria-controls="mobile-menu"
            aria-label={mobileMenuOpen ? t('navigation.close') : t('navigation.menu')}
          >
            <span className="sr-only">
              {mobileMenuOpen ? t('navigation.close') : t('navigation.menu')}
            </span>
            <svg 
              className="w-6 h-6" 
              fill="none" 
              stroke="currentColor" 
              viewBox="0 0 24 24"
              aria-hidden="true"
            >
              {mobileMenuOpen ? (
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
              ) : (
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
              )}
            </svg>
          </button>

          <div
            id="mobile-menu"
            role="dialog"
            aria-label={t('navigation.mobile_menu')}
            aria-modal="true"
            className={`md:hidden fixed inset-0 z-40 transform ${
              mobileMenuOpen ? 'translate-x-0' : 'translate-x-full'
            } transition-transform duration-300 ease-in-out`}
          >
            <div 
              className="fixed inset-0 bg-gray-600 bg-opacity-75 transition-opacity"
              aria-hidden="true"
              onClick={() => setMobileMenuOpen(false)}
            />
            <nav 
              className="relative flex flex-col h-full w-full max-w-sm ml-auto bg-white shadow-xl"
              role="navigation"
              aria-label={t('navigation.mobile')}
            >
              <div className="flex items-center justify-between p-4 border-b border-gray-200">
                <span className="text-lg font-semibold text-gray-800">{t('navigation.menu')}</span>
                <button
                  onClick={() => setMobileMenuOpen(false)}
                  className="p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
                  aria-label={t('navigation.close')}
                >
                  <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                  </svg>
                </button>
              </div>

              <div className="flex-1 overflow-y-auto">
                <div className="px-2 pt-2 pb-3 space-y-1">
                  <NavLink 
                    to="/dev-tools"
                    className={({isActive}) => 
                      `block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
                        isActive 
                          ? 'bg-indigo-50 text-indigo-600' 
                          : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
                      }`
                    }
                    onClick={() => setMobileMenuOpen(false)}
                  >
                    {t('dev-tools.title')}
                  </NavLink>
                  <NavLink 
                    to="/image-tools" 
                    className={({isActive}) => 
                      `block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
                        isActive 
                          ? 'bg-indigo-50 text-indigo-600' 
                          : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
                      }`
                    }
                    onClick={() => setMobileMenuOpen(false)}
                  >
                    {t('image-tools.title')}
                  </NavLink>
                  <NavLink 
                    to="/ai-products" 
                    className={({isActive}) => 
                      `block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
                        isActive 
                          ? 'bg-indigo-50 text-indigo-600' 
                          : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
                      }`
                    }
                    onClick={() => setMobileMenuOpen(false)}
                  >
                    {t('ai-products.title')}
                  </NavLink>
                  <NavLink 
                    to="/blog" 
                    className={({isActive}) => 
                      `block px-4 py-3 rounded-lg text-base font-medium transition-colors duration-200 ${
                        isActive 
                          ? 'bg-indigo-50 text-indigo-600' 
                          : 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600'
                      }`
                    }
                    onClick={() => setMobileMenuOpen(false)}
                  >
                    {t('blog.title')}
                  </NavLink>
                </div>
              </div>

              <div className="border-t border-gray-200 p-4">
                {!user && (
                  <NavLink
                    to="/login"
                    className="flex items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 transition-colors duration-200 shadow-sm hover:shadow-md w-full"
                    onClick={() => setMobileMenuOpen(false)}
                  >
                    {t('login')}
                  </NavLink>
                )}
              </div>
            </nav>
          </div>
        </div>
      </nav>
    </header>
  );
}
export default Header;