feat: 优化网站性能、响应式设计和测试覆盖率
- 更新next.config.ts配置以优化图片和静态资源 - 优化字体加载策略,减少首屏阻塞 - 使用Next.js Image组件替换img标签并实现懒加载 - 重构移动端菜单交互,提升触摸体验 - 新增安全测试和可访问性测试用例 - 修复导航栏滚动定位问题 - 更新部署就绪测试脚本 - 添加相关文档说明优化细节
This commit is contained in:
+6
-6
@@ -10,15 +10,15 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
display: "optional",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
display: "optional",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const notoSansSC = Noto_Sans_SC({
|
||||
@@ -33,8 +33,8 @@ const maShanZheng = Ma_Shan_Zheng({
|
||||
weight: "400",
|
||||
variable: "--font-ma-shan-zheng",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
preload: true,
|
||||
display: "optional",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const longCang = Long_Cang({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Mail, Phone, MapPin } from 'lucide-react';
|
||||
import { COMPANY_INFO, NAVIGATION } from '@/lib/constants';
|
||||
|
||||
@@ -9,7 +10,14 @@ export function Footer() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
|
||||
<div className="lg:col-span-1">
|
||||
<div className="flex items-center mb-6">
|
||||
<img src="/logo.svg" alt={COMPANY_INFO.name} className="h-10 w-auto" />
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt={COMPANY_INFO.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-auto"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[#5C5C5C] text-sm leading-relaxed mb-6">
|
||||
{COMPANY_INFO.description}
|
||||
|
||||
@@ -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)}>
|
||||
联系我们
|
||||
|
||||
Reference in New Issue
Block a user