feat: 重构联系页面并增强安全性

refactor: 优化导航和路由逻辑

fix: 修复移动端样式问题

perf: 优化字体加载和性能

test: 添加安全性和可访问性测试

style: 调整按钮和表单样式

chore: 更新依赖版本

ci: 添加安全头配置

build: 优化构建配置

docs: 更新常量信息
This commit is contained in:
张翔
2026-03-01 10:56:54 +08:00
parent 13c4a2ca49
commit 9cbc80742a
24 changed files with 1087 additions and 440 deletions
+86 -90
View File
@@ -1,73 +1,60 @@
'use client';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { Suspense, useState, useEffect, useCallback, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
import { Menu, X } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { COMPANY_INFO, NAVIGATION, type NavigationItem } from '@/lib/constants';
import { useFocusTrap } from '@/hooks/use-focus-trap';
export function Header() {
function HeaderContent() {
const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [activeSection, setActiveSection] = useState('home');
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
const sectionCacheRef = useRef(new Map<string, { offsetTop: number; offsetHeight: number }>());
const activeSectionRef = useRef(activeSection);
const isManualNavigationRef = useRef(false);
const manualNavTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const getActiveSection = useCallback(() => {
if (pathname === '/contact') return 'contact';
if (pathname === '/') {
const section = searchParams.get('section');
return section || 'home';
}
return '';
}, [pathname, searchParams]);
const activeSection = getActiveSection();
useEffect(() => {
activeSectionRef.current = activeSection;
}, [activeSection]);
useEffect(() => {
let ticking = false;
const updateSectionCache = () => {
const sections = ['home', 'services', 'products', 'cases', 'about', 'news', 'contact'];
sections.forEach(sectionId => {
const element = document.getElementById(sectionId);
if (element) {
sectionCacheRef.current.set(sectionId, {
offsetTop: element.offsetTop,
offsetHeight: element.offsetHeight
});
}
});
};
updateSectionCache();
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
setIsScrolled(window.scrollY > 20);
if (pathname === '/' && !isManualNavigationRef.current) {
const scrollPosition = window.scrollY + 100;
const sections = ['home', 'services', 'products', 'cases', 'about', 'news', 'contact'];
let currentSection = 'home';
setIsScrolled(window.scrollY > 20);
if (pathname === '/' && !isScrollingRef.current) {
const scrollPosition = window.scrollY + 100;
const sections = ['home', 'services', 'products', 'cases', 'about', 'news'];
for (const sectionId of sections) {
const element = document.getElementById(sectionId);
if (element) {
const offsetTop = element.offsetTop;
const offsetHeight = element.offsetHeight;
for (const sectionId of sections) {
const cached = sectionCacheRef.current.get(sectionId);
if (cached && scrollPosition >= cached.offsetTop && scrollPosition < cached.offsetTop + cached.offsetHeight) {
currentSection = sectionId;
break;
if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) {
const currentSection = searchParams.get('section') || 'home';
if (currentSection !== sectionId) {
const url = sectionId === 'home' ? '/' : `/?section=${sectionId}`;
window.history.replaceState(null, '', url);
}
}
if (currentSection !== activeSectionRef.current) {
setActiveSection(currentSection);
break;
}
}
ticking = false;
});
ticking = true;
}
}
};
@@ -79,18 +66,15 @@ export function Header() {
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('keydown', handleGlobalKeyDown);
window.addEventListener('resize', updateSectionCache);
handleScroll();
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('keydown', handleGlobalKeyDown);
window.removeEventListener('resize', updateSectionCache);
if (manualNavTimeoutRef.current) {
clearTimeout(manualNavTimeoutRef.current);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [pathname, isOpen]);
}, [pathname, isOpen, searchParams]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -102,52 +86,56 @@ export function Header() {
}
}, [isOpen]);
const handleNavClick = useCallback((item: NavigationItem) => {
if (pathname === '/' && item.href.startsWith('/#')) {
isManualNavigationRef.current = true;
if (manualNavTimeoutRef.current) {
clearTimeout(manualNavTimeoutRef.current);
}
setActiveSection(item.id);
const targetElement = document.getElementById(item.id);
if (targetElement) {
const checkScrollComplete = () => {
const targetPosition = targetElement.offsetTop;
const currentPosition = window.scrollY;
const threshold = 100;
if (Math.abs(currentPosition - targetPosition) < threshold) {
manualNavTimeoutRef.current = setTimeout(() => {
isManualNavigationRef.current = false;
}, 500);
} else {
requestAnimationFrame(checkScrollComplete);
}
};
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, item: NavigationItem) => {
e.preventDefault();
if (item.id === 'contact') {
router.push('/contact');
} else if (item.id === 'home') {
if (pathname === '/') {
isScrollingRef.current = true;
window.scrollTo({ top: 0, behavior: 'smooth' });
window.history.pushState(null, '', '/');
requestAnimationFrame(checkScrollComplete);
scrollTimeoutRef.current = setTimeout(() => {
isScrollingRef.current = false;
}, 1000);
} else {
manualNavTimeoutRef.current = setTimeout(() => {
isManualNavigationRef.current = false;
}, 2000);
router.push('/');
}
} else {
if (pathname === '/') {
const element = document.getElementById(item.id);
if (element) {
isScrollingRef.current = true;
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.history.pushState(null, '', `/?section=${item.id}`);
scrollTimeoutRef.current = setTimeout(() => {
isScrollingRef.current = false;
}, 1000);
}
} else {
router.push(`/?section=${item.id}`);
}
}
setIsOpen(false);
}, [pathname]);
}, [pathname, router]);
const isActive = useCallback((item: NavigationItem) => {
if (item.id === 'contact') {
return pathname === '/contact';
}
if (pathname === '/') {
return activeSection === item.id;
}
const navPath = item.href.split('#')[0];
return pathname === navPath || pathname.startsWith(navPath + '/');
return false;
}, [pathname, activeSection]);
const navigationItems = useMemo(() => NAVIGATION, []);
const navigationItems = NAVIGATION;
return (
<>
@@ -182,7 +170,7 @@ export function Header() {
<Link
key={item.id}
href={item.href}
onClick={() => handleNavClick(item)}
onClick={(e) => handleNavClick(e, item)}
className={`
relative px-3 py-1.5 text-sm font-medium
transition-all duration-300
@@ -267,7 +255,7 @@ export function Header() {
>
<Link
href={item.href}
onClick={() => handleNavClick(item)}
onClick={(e) => handleNavClick(e, item)}
className={`
block px-4 py-4 text-base font-medium rounded-lg
transition-all duration-200
@@ -301,3 +289,11 @@ export function Header() {
</>
);
}
export function Header() {
return (
<Suspense fallback={<div className="h-16" />}>
<HeaderContent />
</Suspense>
);
}