50天50个小项目 (React19 + Tailwindcss V4) ✨ | 页面布局 与 React-Router-Dom 路由配置
·
🖤 Build Daily. Build Differently.
今天我们将基于的 React19 + Tailwindcss V4 项目,进一步拆分页面结构,引入React-Router-Dom,构建基础导航与页面路由体系。
仓库地址:https://gitee.com/hhm-hhm/50days50projects.git
🧩 1. 页面结构设计
我们的页面将包含以下几个主要模块:
🔗 Nav:导航栏组件,链接各个页面
🏠 Home:首页介绍
📦 Projects:展示 50 个项目列表
ℹ️ About:关于本站与项目介绍
📁 2. 创建组件结构
├── src
│ └── sections
│ ├── Nav.tsx
│ ├── Home.tsx
│ ├── Projects.tsx
│ └── About.tsx (未完成)
│ └── router
│ ├── index.tsx
│ └── constants
│ ├── index.ts
│ └── assets
│ ├── logo.ico
│ ├── ...其他静态文件
你可以在仓库的对应文件夹下面找到所需要的静态文件:50days50projects
💻 3. 代码
Nav.tsx
// Nav.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { navList } from '../constants/index'; // 假设 navList 是一个数组,结构类似 [{ id: number; title: string; link: string }, ...]
import logo from '@/assets/react.svg'; // 使用相对路径导入图片
const Nav: React.FC = () => {
const [currentTab, setCurrentTab] = useState<number>(1);
const navigate = useNavigate();
const handleNavClick = (id: number, link: string) => (e: React.MouseEvent) => {
e.preventDefault();
setCurrentTab(id);
// 如果是首页链接(比如 '/'),直接 navigate;否则也可处理外部链接
if (link.startsWith('/')) {
navigate(link);
} else {
// 可选:如果是外部链接(如 '#projects'),可滚动到对应 section
// 这里假设 link 是页面内锚点(如 '#projects')
const el = document.querySelector(link);
el?.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<header className="fixed top-0 right-0 left-0 z-50 bg-black/90">
<div className="mx-auto max-w-7xl">
<div className="flex items-center justify-between py-5 px-4 sm:px-6 lg:px-8">
{/* Logo */}
<div className="flex items-center justify-center gap-2">
<img src={logo} alt="Logo" className="h-20 w-20" />
</div>
{/* Navigation Links */}
<div className="flex items-center justify-center gap-10">
{navList.map((item) => (
<div
key={item.id}
onClick={handleNavClick(item.id, item.link)} // 更好的方式见下方说明
className={`font-mono text-gray-200 transition-all hover:scale-105 cursor-pointer ${
currentTab === item.id ? 'border-b-2 border-blue-400' : 'border-b-0'
}`}
>
<span>{item.title}</span>
</div>
))}
</div>
</div>
</div>
</header>
);
};
export default Nav;
Home.tsx
// Home.tsx
import React , { useState } from 'react';
import showcaseBg from '../assets/cover.png'; // 确保路径正确
const Home: React.FC = () => {
const [showEffect, setShowEffect] = useState(false);
const handleStartClick = () => {
// 显示点击特效
setShowEffect(true);
// 300ms 后滚动到 Projects(等动画稍作展示)
setTimeout(() => {
const projectsSection = document.querySelector('#Projects');
if (projectsSection) {
projectsSection.scrollIntoView({ behavior: 'smooth' });
}
// 动画结束后隐藏(可选,也可靠 CSS 自动隐藏)
setShowEffect(false);
}, 300);
};
return (
<section className="flex min-h-screen w-full flex-col relative" id="Home">
{/* 背景图 */}
<div
className="absolute inset-0 z-0 opacity-20 bg-center bg-no-repeat"
style={{
backgroundImage: `url(${showcaseBg})`,
}}
/>
{/* 内容区域 */}
<div className="z-10 mx-auto mt-32 flex w-full flex-col gap-y-20 text-center font-mono font-medium text-gray-200 sm:mt-60 px-4 sm:px-6 lg:px-8">
<p className="text-2xl sm:text-3xl">
50 天 50 个项目
<span
className="inline-block ml-2"
style={{
animation: 'wave-animation 2.5s infinite',
transformOrigin: '70% 70%',
}}
>
👋
</span>
</p>
<p className="text-4xl sm:text-5xl md:text-6xl font-bold head-text">
50 个独特的迷你项目来提升你的 HTML、CSS 和 JavaScript 技能
</p>
<button onClick={handleStartClick} className="btn border-2 font-mono py-3 px-6 rounded-md hover:bg-white/10 transition-colors duration-200">
🚀让我们开始吧!🚀
</button>
{/* 点击特效 SVG(绝对定位在按钮位置) */}
{showEffect && (
<div
className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
>
<svg
className="animate-pop-in"
viewBox="0 0 1760 1024"
width="200"
height="200"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M85.504 527.584L22.08 590.496l124.544 2.144 35.968-31.36z" fill="#FF6464" />
<path d="M277.504 401.536l-121.92-130.144-67.808-1.184 134.336 130.368z" fill="#FFE100" />
<path d="M824.32 17.696l64-0.672-16.768 127.712-34.4-0.608z" fill="#65D556" />
<path d="M576.64 100.64l57.76-27.68 38.784 122.848-31.424 13.984z" fill="#FFE100" />
<path d="M767.04 182.976l-31.584-23.552-0.416 22.976 12.992 9.28z" fill="#FF6464" />
<path d="M415.872 247.264l-31.584-23.552-0.416 22.976 12.992 9.28z" fill="#FF6464" />
<path d="M609.408 922.464l-78.976 24.864 71.552-99.424 41.92-15.84z" fill="#FFE100" />
<path d="M289.824 861.024L200.128 1000.96l52.64-6.464 90.56-140.096z" fill="#65D556" />
<path d="M160.704 728.576l28.96-26.688 2.784 22.816-11.936 10.56z" fill="#00C4FF" />
<path d="M256.96 152.864l28.96-26.688 2.816 22.784-11.936 10.56z" fill="#00C4FF" />
<path d="M31.136 883.424l12.064 12.48-17.696 20.48-13.824-14.464z" fill="#FF6464" />
<path d="M816.192 895.456l12.896-1.6-8.992 33.376-18.88 2.304z" fill="#00C4FF" />
<path d="M1316.032 927.232l58.752-8.576-30.592-118.944-31.456 5.536z" fill="#FF6464" />
<path d="M1654.752 799.264l103.84 129.856-53.024-0.96-104.704-129.824z" fill="#00C4FF" />
<path d="M1440.064 490.784l97.824 53.248 61.728-24.704-108.8-40.128z" fill="#FFE100" />
<path d="M1023.264 797.6l42.528 102.944 63.488 19.648-59.264-99.648z" fill="#FFE100" />
<path d="M1447.584 681.664l-10.688 13.632 19.712 18.528 12.224-15.808z" fill="#65D556" />
<path d="M1537.6 811.36l14.304 14.176-19.072 7.104-43.424-40.512 25.408 0.448 22.784 18.784z" fill="#FFE100" />
<path d="M1411.328 271.968l129.056-143.296-1.376 79.104-128.544 114.72 0.864-50.528z" fill="#FFE100" />
<path d="M1149.76 254.848l44.16-186.56 55.744-35.712-54.336 193.088z" fill="#65D556" />
<path d="M1267.456 245.248l-14.944 10.944 19.136 6.336 45.312-31.232-25.792-0.448z" fill="#FF6464" />
<path d="M1552.256 331.424l-12.992-0.224 12.416 32.224 19.008 0.32z" fill="#65D556" />
</svg>
</div>
)}
</div>
</section>
);
};
export default Home;
Projects.tsx
// Projects.tsx
import React from 'react';
import { projectList } from '../constants'; // 假设 projectList 是一个数组
import { Link } from 'react-router-dom'; // 如果使用 React Router
const Projects: React.FC = () => {
return (
<section className="px-4 sm:px-6 lg:px-8 py-12" id="Projects">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
{projectList.map((item) => (
<div
key={item.id}
className="relative overflow-hidden rounded-lg shadow-lg"
>
<img
src={item.image}
alt={item.title}
className="w-full h-auto object-cover aspect-video"
/>
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/70 font-mono opacity-0 transition-opacity duration-300 ease-in-out hover:opacity-100">
<h3 className="mb-4 text-xl font-bold text-white">{item.title}</h3>
<Link
to={item.link}
aria-label={`View live demo of ${item.title}`}
className="rounded-md bg-blue-500 px-4 py-2 text-white transition-colors duration-200 hover:bg-blue-600"
>
Live Demo
</Link>
</div>
</div>
))}
</div>
</section>
);
};
export default Projects;
src/router/index.tsx
// src/router/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import App from '@/App';
const routes: RouteObject[] = [
{
path: '/',
element: <App />,
children: [
{
path: '/ExpandingCards',
lazy: () => import( '@/projects/ExpandingCards.tsx').then(mod => ({ Component: mod.default })),
},
],
},
{
path: '/about',
lazy: () => import('@/pages/AboutPage').then(mod => ({ Component: mod.default })),
},
{
path: '/test',
lazy: () => import('@/pages/TestPage').then(mod => ({ Component: mod.default })),
},
{
path: '/projects',
lazy: () => import('@/sections/Projects').then(mod => ({ Component: mod.default })),
},
{
path: '/nav',
lazy: () => import('@/sections/Nav').then(mod => ({ Component: mod.default })),
},
];
const router = createBrowserRouter(routes);
export default router;
App.tsx
// import {BrowserRouter as Router, Route,Link} from 'react-router-dom'
import { Outlet,useLocation } from 'react-router-dom';
import Navbar from './sections/Nav'
import Home from './sections/Home'
import Projects from './sections/Projects'
import Bottom from './sections/Bottom'
import Footer from './sections/Footer'
const App = () => {
const location = useLocation();
// 如果不是首页,只渲染子路由(通过 <Outlet />)
if (location.pathname !== '/') {
return <Outlet />;
}
// 首页:渲染完整布局
return (
<section className="max-w-8xl mx-auto">
<Navbar />
<Home />
<Projects />
<Bottom />
<Footer />
</section>
)
}
export default App
main.tsx
// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import router from './router';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
src/constants/index.ts
export interface NavItem {
id: number;
title: string;
link: string; // 如 '/', '#projects', '/about' 等
}
export const navList: NavItem[] = [
{ id: 1, title: 'Home', link: '#Home' },
{ id: 2, title: 'Peoject', link: '#Projects' },
{ id: 3, title: 'About', link: '#About' },
]
interface ProjectItem {
id: number | string;
title: string;
image: string; // 可以是 import 的模块或字符串路径
link: string; // 如 "/projects/1"
}
export const projectList: ProjectItem[] = [
{
id: 1,
title: 'Expanding Cards',
image: 'https://50projects50days.com/img/projects-img/1-expanding-cards.png',
link: 'ExpandingCards',
},
]
✅ 4. 当前完成状态
Nav组件(包含导航链接)
Home组件(展示欢迎语与介绍)
Projects组件(展示项目概览)
About组件
Router配置完成并接入 App.tsx
🚀 小结
本日我们构建了基础页面架构与React-Router 路由系统,为后续页面跳转与模块拆分打下基础。
📅 下篇预告:开发 Day01 的组件内容「Expanding Cards」
参考链接:https://blog.csdn.net/qq_44808710/article/details/148044494
更多推荐
所有评论(0)