refactor(ui): 优化导航组件和页面布局
- 移除多个页面的面包屑导航组件 - 添加统一的返回按钮组件替代各页面独立实现 - 优化导航栏滚动检测逻辑和动画效果 - 更新常量类型定义和统计数据 - 调整动态导入的SSR配置为false - 添加FlipClock组件展示公司运营时长 - 优化新闻列表页的类型安全和响应式设计
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { COMPANY_INFO, NAVIGATION } from '@/lib/constants';
|
||||
import { COMPANY_INFO, NAVIGATION, type NavigationItem } from '@/lib/constants';
|
||||
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
||||
|
||||
export function Header() {
|
||||
@@ -15,25 +15,58 @@ export function Header() {
|
||||
const [activeSection, setActiveSection] = useState('home');
|
||||
const pathname = usePathname();
|
||||
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);
|
||||
|
||||
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 = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
|
||||
if (pathname === '/') {
|
||||
const sections = ['home', 'services', 'products', 'cases', 'about', 'news', 'contact'];
|
||||
const scrollPosition = window.scrollY + 100;
|
||||
|
||||
for (const sectionId of sections) {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
const { offsetTop, offsetHeight } = element;
|
||||
if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) {
|
||||
setActiveSection(sectionId);
|
||||
break;
|
||||
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';
|
||||
|
||||
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 (currentSection !== activeSectionRef.current) {
|
||||
setActiveSection(currentSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +78,16 @@ 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);
|
||||
}
|
||||
};
|
||||
}, [pathname, isOpen]);
|
||||
|
||||
@@ -63,14 +101,32 @@ export function Header() {
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const isActive = (item: typeof NAVIGATION[number]) => {
|
||||
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);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
const isActive = useCallback((item: NavigationItem) => {
|
||||
if (pathname === '/') {
|
||||
return activeSection === item.id;
|
||||
}
|
||||
|
||||
const navPath = item.href.split('#')[0];
|
||||
return pathname === navPath || pathname.startsWith(navPath + '/');
|
||||
};
|
||||
}, [pathname, activeSection]);
|
||||
|
||||
const navigationItems = useMemo(() => NAVIGATION, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,10 +154,11 @@ export function Header() {
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1" role="navigation" aria-label="主导航">
|
||||
{NAVIGATION.map((item) => (
|
||||
{navigationItems.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
onClick={() => handleNavClick(item)}
|
||||
className={`
|
||||
relative px-3 py-1.5 text-sm font-medium
|
||||
transition-all duration-300
|
||||
@@ -113,13 +170,16 @@ export function Header() {
|
||||
aria-current={isActive(item) ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
{isActive(item) && (
|
||||
<motion.span
|
||||
layoutId="activeNav"
|
||||
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-[#C41E3A] rounded-full"
|
||||
transition={{ type: "spring", stiffness: 380, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`
|
||||
absolute bottom-0 left-1/2 -translate-x-1/2 w-6 h-0.5 bg-[#C41E3A] rounded-full
|
||||
transition-all duration-200 ease-out
|
||||
${isActive(item)
|
||||
? 'opacity-100 scale-x-100'
|
||||
: 'opacity-0 scale-x-0'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
@@ -172,7 +232,7 @@ export function Header() {
|
||||
aria-label="移动端导航"
|
||||
>
|
||||
<nav className="container-wide py-4">
|
||||
{NAVIGATION.map((item, index) => (
|
||||
{navigationItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
@@ -181,7 +241,7 @@ export function Header() {
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
onClick={() => handleNavClick(item)}
|
||||
className={`
|
||||
block px-4 py-3 text-base font-medium
|
||||
transition-all duration-300
|
||||
|
||||
Reference in New Issue
Block a user