feat: 优化网站性能、响应式设计和测试覆盖率

- 更新next.config.ts配置以优化图片和静态资源
- 优化字体加载策略,减少首屏阻塞
- 使用Next.js Image组件替换img标签并实现懒加载
- 重构移动端菜单交互,提升触摸体验
- 新增安全测试和可访问性测试用例
- 修复导航栏滚动定位问题
- 更新部署就绪测试脚本
- 添加相关文档说明优化细节
This commit is contained in:
张翔
2026-02-28 22:32:45 +08:00
parent 7b2a8af19f
commit 13c4a2ca49
14 changed files with 2748 additions and 789 deletions
+48 -21
View File
@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { Menu, X } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
@@ -103,16 +104,36 @@ export function Header() {
const handleNavClick = useCallback((item: NavigationItem) => {
if (pathname === '/' && item.href.startsWith('/#')) {
setActiveSection(item.id);
isManualNavigationRef.current = true;
if (manualNavTimeoutRef.current) {
clearTimeout(manualNavTimeoutRef.current);
}
manualNavTimeoutRef.current = setTimeout(() => {
isManualNavigationRef.current = false;
}, 800);
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);
}
};
requestAnimationFrame(checkScrollComplete);
} else {
manualNavTimeoutRef.current = setTimeout(() => {
isManualNavigationRef.current = false;
}, 2000);
}
}
setIsOpen(false);
}, [pathname]);
@@ -146,10 +167,13 @@ export function Header() {
href="/"
className="flex items-center group"
>
<img
<Image
src="/logo.svg"
alt={COMPANY_INFO.name}
width={32}
height={32}
className="h-8 w-auto transition-transform duration-200 group-hover:scale-105"
priority
/>
</Link>
@@ -194,48 +218,50 @@ export function Header() {
</div>
<button
className="md:hidden p-2 -mr-2 text-[#3D3D3D] hover:text-[#1C1C1C] transition-colors"
className="md:hidden p-3 -mr-3 text-[#3D3D3D] hover:text-[#1C1C1C] hover:bg-[#F5F5F5] rounded-lg transition-all duration-200 active:scale-95"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
aria-controls="mobile-menu"
aria-label={isOpen ? '关闭菜单' : '打开菜单'}
style={{ minWidth: '44px', minHeight: '44px' }}
>
{isOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
{isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
</header>
<AnimatePresence>
<AnimatePresence mode="wait">
{isOpen && (
<motion.div
ref={focusTrapRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40 md:hidden"
>
<div
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="absolute top-16 left-0 right-0 bg-white/95 backdrop-blur-xl border-b border-[#E2E8F0] shadow-lg"
className="absolute top-16 right-0 bottom-0 left-0 bg-white/98 backdrop-blur-xl shadow-2xl overflow-y-auto"
id="mobile-menu"
role="navigation"
aria-label="移动端导航"
>
<nav className="container-wide py-4">
<nav className="container-wide py-6">
{navigationItems.map((item, index) => (
<motion.div
key={item.id}
initial={{ x: -20, opacity: 0 }}
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: index * 0.05 }}
>
@@ -243,23 +269,24 @@ export function Header() {
href={item.href}
onClick={() => handleNavClick(item)}
className={`
block px-4 py-3 text-base font-medium
transition-all duration-300
border-l-2
block px-4 py-4 text-base font-medium rounded-lg
transition-all duration-200
${isActive(item)
? 'text-[#1C1C1C] border-[#1C1C1C] bg-[#F5F5F5]'
: 'text-[#3D3D3D] border-transparent hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
? 'text-[#1C1C1C] bg-[#F5F5F5] border-l-4 border-[#C41E3A]'
: 'text-[#3D3D3D] hover:text-[#1C1C1C] hover:bg-[#F5F5F5]'
}
`}
style={{ minHeight: '48px', display: 'flex', alignItems: 'center' }}
>
{item.label}
</Link>
</motion.div>
))}
<div className="mt-4 px-4 pt-4 border-t border-[#E2E8F0] space-y-3">
<div className="mt-6 px-4 pt-6 border-t border-[#E2E8F0]">
<Button
className="w-full"
asChild
size="lg"
>
<Link href="/contact" onClick={() => setIsOpen(false)}>