refactor(导航): 将哈希路由改为标准路径路由 #13
@@ -1,66 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense, useState, useEffect, useCallback, useRef } from 'react';
|
import { Suspense, useState, useEffect, useCallback } from 'react';
|
||||||
import { StaticLink } from '@/components/ui/static-link';
|
import { StaticLink } from '@/components/ui/static-link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { COMPANY_INFO, NAVIGATION, type NavigationItem } from '@/lib/constants';
|
import { COMPANY_INFO, NAVIGATION, type NavigationItem } from '@/lib/constants';
|
||||||
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__isProgrammaticScroll?: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function HeaderContent() {
|
function HeaderContent() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
|
const focusTrapRef = useFocusTrap<HTMLDivElement>(isOpen);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setIsScrolled(window.scrollY > 20);
|
setIsScrolled(window.scrollY > 20);
|
||||||
|
|
||||||
if (pathname === '/' && !isScrollingRef.current && !window.__isProgrammaticScroll) {
|
|
||||||
const scrollPosition = window.scrollY + 100;
|
|
||||||
const sections = ['home', 'services', 'solutions', 'products', 'cases', 'about', 'team', 'news'];
|
|
||||||
|
|
||||||
for (const sectionId of sections) {
|
|
||||||
const element = document.getElementById(sectionId);
|
|
||||||
if (element) {
|
|
||||||
const offsetTop = element.offsetTop;
|
|
||||||
const offsetHeight = element.offsetHeight;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -75,11 +33,8 @@ function HeaderContent() {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
window.removeEventListener('keydown', handleGlobalKeyDown);
|
window.removeEventListener('keydown', handleGlobalKeyDown);
|
||||||
if (scrollTimeoutRef.current) {
|
|
||||||
clearTimeout(scrollTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [pathname, isOpen, searchParams]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
@@ -93,57 +48,21 @@ function HeaderContent() {
|
|||||||
|
|
||||||
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, item: NavigationItem) => {
|
const handleNavClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, item: NavigationItem) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
window.location.href = item.href;
|
||||||
if (item.id === 'contact') {
|
|
||||||
window.location.href = '/contact';
|
|
||||||
} else if (item.id === 'home') {
|
|
||||||
if (pathname === '/') {
|
|
||||||
isScrollingRef.current = true;
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
window.history.pushState(null, '', '/');
|
|
||||||
|
|
||||||
scrollTimeoutRef.current = setTimeout(() => {
|
|
||||||
isScrollingRef.current = false;
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (pathname === '/') {
|
|
||||||
const scrollToSection = (retryCount = 0) => {
|
|
||||||
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 if (retryCount < 10) {
|
|
||||||
setTimeout(() => scrollToSection(retryCount + 1), 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
scrollToSection();
|
|
||||||
} else {
|
|
||||||
window.location.href = `/?section=${item.id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, [pathname]);
|
}, []);
|
||||||
|
|
||||||
const isActive = useCallback((item: NavigationItem) => {
|
const isActive = useCallback((item: NavigationItem) => {
|
||||||
if (item.id === 'contact') {
|
if (item.id === 'contact') {
|
||||||
return pathname === '/contact';
|
return pathname === '/contact';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === '/') {
|
if (item.id === 'home') {
|
||||||
return activeSection === item.id;
|
return pathname === '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return pathname === `/${item.id}`;
|
||||||
}, [pathname, activeSection]);
|
}, [pathname]);
|
||||||
|
|
||||||
const navigationItems = NAVIGATION;
|
const navigationItems = NAVIGATION;
|
||||||
|
|
||||||
|
|||||||
@@ -26,19 +26,16 @@ export function MobileMenu({ className }: MobileMenuProps) {
|
|||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleNavClick = (id: string) => {
|
const handleNavClick = (href: string) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
const element = document.getElementById(id);
|
window.location.href = href;
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent, id?: string) => {
|
const handleKeyDown = (event: React.KeyboardEvent, href?: string) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (id) {
|
if (href) {
|
||||||
handleNavClick(id);
|
handleNavClick(href);
|
||||||
} else {
|
} else {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}
|
}
|
||||||
@@ -84,8 +81,8 @@ export function MobileMenu({ className }: MobileMenuProps) {
|
|||||||
{NAVIGATION.map((item) => (
|
{NAVIGATION.map((item) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavClick(item.id)}
|
onClick={() => handleNavClick(item.href)}
|
||||||
onKeyDown={(e) => handleKeyDown(e, item.id)}
|
onKeyDown={(e) => handleKeyDown(e, item.href)}
|
||||||
className="block w-full text-left px-4 py-4 text-[#171717] hover:bg-[#FEF2F4] hover:text-[#C41E3A] rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-inset min-h-[48px]"
|
className="block w-full text-left px-4 py-4 text-[#171717] hover:bg-[#FEF2F4] hover:text-[#C41E3A] rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-[#C41E3A] focus:ring-inset min-h-[48px]"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
|||||||
@@ -76,19 +76,19 @@ describe('MobileTabBar', () => {
|
|||||||
it('should have correct href for services', () => {
|
it('should have correct href for services', () => {
|
||||||
render(<MobileTabBar />);
|
render(<MobileTabBar />);
|
||||||
const servicesLink = screen.getByText('服务').closest('a');
|
const servicesLink = screen.getByText('服务').closest('a');
|
||||||
expect(servicesLink).toHaveAttribute('href', '/#services');
|
expect(servicesLink).toHaveAttribute('href', '/services');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct href for products', () => {
|
it('should have correct href for products', () => {
|
||||||
render(<MobileTabBar />);
|
render(<MobileTabBar />);
|
||||||
const productsLink = screen.getByText('产品').closest('a');
|
const productsLink = screen.getByText('产品').closest('a');
|
||||||
expect(productsLink).toHaveAttribute('href', '/#products');
|
expect(productsLink).toHaveAttribute('href', '/products');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct href for news', () => {
|
it('should have correct href for news', () => {
|
||||||
render(<MobileTabBar />);
|
render(<MobileTabBar />);
|
||||||
const newsLink = screen.getByText('新闻').closest('a');
|
const newsLink = screen.getByText('新闻').closest('a');
|
||||||
expect(newsLink).toHaveAttribute('href', '/#news');
|
expect(newsLink).toHaveAttribute('href', '/news');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct href for contact', () => {
|
it('should have correct href for contact', () => {
|
||||||
|
|||||||
@@ -1,56 +1,32 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useLayoutEffect, useRef } from 'react';
|
|
||||||
import { StaticLink } from '@/components/ui/static-link';
|
import { StaticLink } from '@/components/ui/static-link';
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Home, Briefcase, Package, FileText, User } from 'lucide-react';
|
import { Home, Briefcase, Package, FileText, User } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'home', label: '首页', href: '/', icon: Home },
|
{ id: 'home', label: '首页', href: '/', icon: Home },
|
||||||
{ id: 'services', label: '服务', href: '/#services', icon: Briefcase },
|
{ id: 'services', label: '服务', href: '/services', icon: Briefcase },
|
||||||
{ id: 'products', label: '产品', href: '/#products', icon: Package },
|
{ id: 'products', label: '产品', href: '/products', icon: Package },
|
||||||
{ id: 'news', label: '新闻', href: '/#news', icon: FileText },
|
{ id: 'news', label: '新闻', href: '/news', icon: FileText },
|
||||||
{ id: 'contact', label: '联系', href: '/contact', icon: User },
|
{ id: 'contact', label: '联系', href: '/contact', icon: User },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function MobileTabBar() {
|
export function MobileTabBar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const [hash, setHash] = useState('');
|
|
||||||
const isInitializedRef = useRef(false);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!isInitializedRef.current) {
|
|
||||||
isInitializedRef.current = true;
|
|
||||||
setHash(window.location.hash.slice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHashChange = () => {
|
|
||||||
setHash(window.location.hash.slice(1));
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
|
||||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isActive = (_href: string, id: string) => {
|
const isActive = (_href: string, id: string) => {
|
||||||
if (id === 'contact') {
|
if (id === 'contact') {
|
||||||
return pathname === '/contact';
|
return pathname === '/contact';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === '/') {
|
if (id === 'home') {
|
||||||
const section = searchParams.get('section');
|
return pathname === '/';
|
||||||
const currentSection = section || hash;
|
|
||||||
|
|
||||||
if (id === 'home') {
|
|
||||||
return !currentSection || currentSection === 'home';
|
|
||||||
}
|
|
||||||
return currentSection === id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return pathname === `/${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export interface NavigationItem {
|
|||||||
|
|
||||||
export const NAVIGATION: NavigationItem[] = [
|
export const NAVIGATION: NavigationItem[] = [
|
||||||
{ id: 'home', label: '首页', href: '/' },
|
{ id: 'home', label: '首页', href: '/' },
|
||||||
{ id: 'services', label: '核心业务', href: '/' },
|
{ id: 'services', label: '核心业务', href: '/services' },
|
||||||
{ id: 'solutions', label: '解决方案', href: '/' },
|
{ id: 'solutions', label: '解决方案', href: '/solutions' },
|
||||||
{ id: 'products', label: '产品服务', href: '/' },
|
{ id: 'products', label: '产品服务', href: '/products' },
|
||||||
{ id: 'cases', label: '成功案例', href: '/' },
|
{ id: 'cases', label: '成功案例', href: '/cases' },
|
||||||
{ id: 'about', label: '关于我们', href: '/' },
|
{ id: 'about', label: '关于我们', href: '/about' },
|
||||||
{ id: 'news', label: '新闻动态', href: '/' },
|
{ id: 'news', label: '新闻动态', href: '/news' },
|
||||||
{ id: 'contact', label: '联系', href: '/contact' },
|
{ id: 'contact', label: '联系', href: '/contact' },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user