Files
software-workspace/src/components/Nav.tsx
T
shenjianZ 6b58b55c32 feat: 全面重构网站工程化体系与 UI 架构
- 将单体 style.css 拆分为 tokens/reset/fonts/layout/responsive/组件级 CSS 模块
- 从 Google Fonts CDN 迁移至本地自托管字体(JetBrainsMono、NotoSansSC)
- 引入 Vitest + Testing Library 测试体系,新增单元测试
- 添加 GitHub Actions CI 流水线(lint → build → test)
- 新增 Prettier 格式化与 ESLint 规则强化
- 重构全部 YAML 数据文件,完善项目详情页(截图轮播、更新日志)
- 新增项目文档编写指南(docs/create-project.md)
2026-05-22 13:34:41 +08:00

80 lines
2.8 KiB
TypeScript

import { Link, useLocation } from 'react-router-dom';
import { Sun, Moon, Globe, ExternalLink, Menu, X } from 'lucide-react';
import { useApp } from '../contexts/AppContext';
import { useI18n } from '../hooks/useI18n';
import { siteData } from '../data/siteData';
export default function Nav() {
const { theme, mobileMenuOpen, toggleTheme, toggleLang, openMobileMenu, closeMobileMenu } =
useApp();
const { t, bi } = useI18n();
const location = useLocation();
const currentPath = '/' + location.pathname.slice(1);
return (
<>
<nav className="nav">
<div className="nav-inner">
<Link to="/" className="nav-brand">
{siteData.brand.logo ? (
<img src={siteData.brand.logo} alt={bi(siteData.brand.name)} className="nav-brand-icon" />
) : (
<span className="nav-brand-icon">Z</span>
)}
{bi(siteData.brand.name)}
</Link>
<div className="nav-links">
{siteData.nav.map((n) => {
const navPath = n.hash.slice(2) || '/';
const isActive = currentPath === navPath || (navPath === '/' && currentPath === '');
return (
<Link
key={n.id}
to={navPath === '/' ? '/' : navPath}
className={`nav-link${isActive ? ' active' : ''}`}
>
{bi(n.label)}
</Link>
);
})}
</div>
<div className="nav-actions">
<button className="nav-btn" onClick={toggleTheme} title={t('nav.theme')}>
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
</button>
<button className="nav-btn" onClick={toggleLang} title={t('nav.lang')}>
<Globe size={16} />
</button>
<a href={siteData.brand.github} target="_blank" className="nav-btn" title="GitHub">
<ExternalLink size={18} />
</a>
<button
className="nav-btn nav-mobile-toggle"
onClick={mobileMenuOpen ? closeMobileMenu : openMobileMenu}
aria-label="Menu"
aria-expanded={mobileMenuOpen}
>
{mobileMenuOpen ? <X size={18} /> : <Menu size={18} />}
</button>
</div>
</div>
</nav>
<div className={`nav-mobile-menu${mobileMenuOpen ? ' open' : ''}`}>
{siteData.nav.map((n) => {
const navPath = n.hash.slice(2) || '/';
return (
<Link
key={n.id}
to={navPath === '/' ? '/' : navPath}
className="nav-link"
onClick={closeMobileMenu}
>
{bi(n.label)}
</Link>
);
})}
</div>
</>
);
}