🖤 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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐