React 商品搜索与 API 演示项目全解析

这是一篇基于 React + Vite + React Router 构建的完整前端项目博客。
项目实现了:商品搜索(前端静态数据)、多页面路由、深色主题 UI、以及通过 GET/POST 请求调用公开 API 的功能。
本文将逐文件分析代码逻辑、样式设计以及关键 React 概念(Hooks、路由、状态管理、网络请求)。


📁 项目结构

text

src/
├── assets/               # 静态资源(未使用)
├── data/
│   └── products.js       # 商品模拟数据
├── pages/
│   ├── Home.jsx          # 首页组件
│   ├── Home.css
│   ├── Search.jsx        # 商品搜索页组件
│   ├── Search.css
│   ├── ApiPage.jsx       # API 请求演示页组件
│   └── ApiPage.css
├── App.jsx               # 根组件(路由配置 + 导航栏)
├── App.css               # 全局导航栏样式
├── index.css             # 全局重置 + 深色主题
├── main.jsx              # 入口文件(挂载路由)
└── ...

🔧 一、项目入口与全局配置

1. main.jsx – React 根渲染与路由包裹

// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

解释

  • 使用 ReactDOM.createRoot 开启 React 18 的并发渲染。
  • <BrowserRouter> 提供基于 HTML5 History API 的路由上下文,使 <App /> 内部可以使用 useRoutesLink 等路由钩子。
  • 全局引入 index.css 作为基础样式。

2. index.css – 全局深色主题与重置

/* src/index.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body, #root {
  width: 100%;
  min-height: 100%;
  background: #0a0f1c;  /* 深蓝黑背景 */
}

body {
  font-family: 'Segoe UI', 'Inter', system-ui, sans-serif;
  color: #eef2ff;
  line-height: 1.5;
}

::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-track {
  background: #1e293b;
}
::-webkit-scrollbar-thumb {
  background: #3b82f6;
  border-radius: 4px;
}

a {
  text-decoration: none;
  color: inherit;
}

解释

  • 全局盒模型重置,避免边距混乱。
  • 设置全屏深色背景(#0a0f1c),与后续组件风格统一。
  • 自定义滚动条样式,增加蓝绿色点缀。
  • 移除链接默认下划线,保持视觉干净。

🧭 二、根组件与路由导航

3. App.jsx – 导航栏与路由配置

// src/App.jsx
import { Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home";
import Search from "./pages/Search";
import ApiPage from "./pages/ApiPage";
import "./App.css";

function App() {
  return (
    <div>
      <nav className="navbar">
        <Link to="/">🏠 首页</Link>
        <Link to="/search">🔍 商品搜索</Link>
        <Link to="/api">✨ 开放数据</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/search" element={<Search />} />
        <Route path="/api" element={<ApiPage />} />
      </Routes>
    </div>
  );
}

export default App;

解释

  • 使用 react-router-domLink 组件实现无刷新跳转。
  • 导航栏使用 className="navbar",样式在 App.css 中定义。
  • <Routes> 包裹多个 <Route>,匹配当前路径并渲染对应页面组件。
  • 三个页面:首页(Home)、商品搜索(Search)、API 演示(ApiPage)。

4. App.css – 导航栏毛玻璃效果

/* src/App.css */
.navbar {
  background: rgba(15, 23, 42, 0.9);
  backdrop-filter: blur(12px);
  padding: 1rem 2rem;
  display: flex;
  gap: 2rem;
  border-bottom: 1px solid rgba(59, 130, 246, 0.4);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  position: sticky;
  top: 0;
  z-index: 10;
}

.navbar a {
  font-weight: 600;
  font-size: 1.1rem;
  padding: 0.5rem 1rem;
  border-radius: 40px;
  transition: all 0.2s ease;
  background: rgba(255,255,255,0.05);
}

.navbar a:hover {
  background: linear-gradient(135deg, #3b82f6, #10b981);
  color: white;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59,130,246,0.3);
}

解释

  • backdrop-filter: blur(12px) 实现毛玻璃效果,半透明背景。
  • sticky 定位使导航栏固定在顶部。
  • 每个导航链接带圆角、半透背景,hover 时展示蓝绿渐变并轻微上浮。
  • 整体风格与深色主题完美融合。

🏠 三、首页(Home)组件 – 产品介绍与特性卡片

5. Home.jsx – 首页结构

// src/pages/Home.jsx
import { Link } from "react-router-dom";
import "./Home.css";

function Home() {
  return (
    <div className="home-page">
      <div className="hero">
        <h1 className="glow-text">✨ React 商品探索</h1>
        <p className="subtitle">基于 React Router + Hooks 的静态商品搜索演示</p>
        <Link to="/search">
          <button className="cta-button">🔍 立即搜索商品 →</button>
        </Link>
      </div>

      <div className="features">
        <div className="feature-card">
          <div className="feature-icon">⚛️</div>
          <h3>React 函数组件</h3>
          <p>使用 useState 管理状态,useEffect 概念预留扩展</p>
        </div>
        <div className="feature-card">
          <div className="feature-icon">🧭</div>
          <h3>React Router v6</h3>
          <p>声明式路由,支持多页面导航与动态路由</p>
        </div>
        <div className="feature-card">
          <div className="feature-icon">🔎</div>
          <h3>前端模糊搜索</h3>
          <p>本地商品数据,实时筛选不区分大小写</p>
        </div>
      </div>

      <footer className="home-footer">
        <p>💡 提示:点击上方导航或按钮进入搜索页,试试搜索“手机”、“苹果”</p>
      </footer>
    </div>
  );
}

export default Home;

解释

  • 使用 flex 布局实现垂直居中(配合 .home-page 样式)。
  • 标题使用渐变文字(CSS 中定义),带有微弱光晕。
  • 三个特性卡片(feature-card)采用毛玻璃效果、hover 上浮动画。
  • 底部 footer 提供简短的使用提示。
  • 通过 Link 跳转到搜索页。

6. Home.css – 渐变标题、卡片动画

/* src/pages/Home.css */
.home-page {
  min-height: calc(100vh - 70px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.hero h1 {
  font-size: 3.5rem;
  background: linear-gradient(135deg, #60a5fa, #34d399);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

.cta-button {
  background: linear-gradient(95deg, #3b82f6, #10b981);
  border: none;
  padding: 12px 32px;
  border-radius: 40px;
  color: white;
  box-shadow: 0 8px 20px rgba(59,130,246,0.4);
  transition: transform 0.2s, box-shadow 0.2s;
}
.cta-button:hover {
  transform: scale(1.03);
  box-shadow: 0 12px 28px rgba(59,130,246,0.6);
}

.feature-card {
  background: rgba(30, 41, 59, 0.6);
  backdrop-filter: blur(8px);
  border-radius: 28px;
  padding: 1.8rem;
  transition: all 0.3s;
}
.feature-card:hover {
  transform: translateY(-6px);
  border-color: #3b82f6;
}

解释

  • 标题文字采用 background-clip: text 实现蓝绿渐变。
  • 按钮采用相同渐变色,hover 放大并增强阴影,营造“发光”感。
  • 卡片 hover 时向上平移,并增加蓝色边框,交互感强。

🔍 四、商品搜索页(Search) – 静态数据 + 模糊搜索

7. 模拟商品数据 products.js

// src/data/products.js
export const products = [
  { id: 1, name: "苹果 iPhone 15", category: "手机", price: 5999 },
  { id: 2, name: "华为 Mate 60", category: "手机", price: 5499 },
  { id: 3, name: "小米 14 Pro", category: "手机", price: 4999 },
  { id: 4, name: "联想 ThinkPad X1", category: "笔记本", price: 8999 },
  { id: 5, name: "戴尔 XPS 13", category: "笔记本", price: 9999 },
  { id: 6, name: "索尼 WH-1000XM5", category: "耳机", price: 1999 },
  { id: 7, name: "苹果 AirPods Pro 2", category: "耳机", price: 1899 }
];

解释

  • 前端静态数据,模拟商品列表。包含 idnamecategoryprice
  • 搜索时基于 name 字段进行模糊匹配。

8. Search.jsx – 核心搜索逻辑

// src/pages/Search.jsx
import { useState } from "react";
import { products } from "../data/products";
import "./Search.css";

function Search() {
  const [keyword, setKeyword] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const [hasSearched, setHasSearched] = useState(false);

  const handleSearch = () => {
    if (!keyword.trim()) {
      setSearchResults([]);
      setHasSearched(true);
      return;
    }
    const filtered = products.filter(product =>
      product.name.toLowerCase().includes(keyword.toLowerCase())
    );
    setSearchResults(filtered);
    setHasSearched(true);
  };

  const quickSearch = (term) => {
    setKeyword(term);
    const filtered = products.filter(product =>
      product.name.toLowerCase().includes(term.toLowerCase())
    );
    setSearchResults(filtered);
    setHasSearched(true);
  };

  return (
    <div className="search-page">
      <div className="search-container">
        <div className="search-header">
          <h2>✨ 商品搜索</h2>
        </div>
        <div className="search-box">
          <input
            type="text"
            placeholder="输入商品名称,例如:iPhone"
            value={keyword}
            onChange={(e) => setKeyword(e.target.value)}
            onKeyPress={(e) => e.key === "Enter" && handleSearch()}
          />
          <button onClick={handleSearch}>搜索</button>
        </div>
        {hasSearched && (
          <div className="results">
            <div className="result-count">共找到 {searchResults.length} 件商品</div>
            {searchResults.length === 0 ? (
              <div className="empty-result">😞 没有找到相关商品</div>
            ) : (
              <ul className="product-list">
                {searchResults.map(product => (
                  <li key={product.id} className="product-item">
                    <span className="product-name">{product.name}</span>
                    <span className="product-category">{product.category}</span>
                    <span className="product-price">¥{product.price}</span>
                  </li>
                ))}
              </ul>
            )}
          </div>
        )}
        <div className="demo-tips">
          <span>⚡ 试试快速搜索:</span>
          <button onClick={() => quickSearch("手机")}>手机</button>
          <button onClick={() => quickSearch("苹果")}>苹果</button>
          <button onClick={() => quickSearch("笔记本")}>笔记本</button>
        </div>
      </div>
    </div>
  );
}

export default Search;

解释

  • 使用 useState 管理关键词、搜索结果、是否已搜索过。
  • handleSearch 执行模糊匹配(转小写后 includes),更新结果。
  • 按回车键(onKeyPress)同样触发搜索。
  • quickSearch 函数在设置关键词的同时立即执行搜索,避免异步状态延迟。
  • 搜索结果渲染为自定义列表项,每个商品展示名称、分类、价格。
  • 快速搜索按钮方便演示。

9. Search.css – 深色卡片与搜索结果样式

.search-box {
  display: flex;
  gap: 12px;
  background: rgba(30, 41, 59, 0.5);
  backdrop-filter: blur(8px);
  border-radius: 60px;
  border: 1px solid rgba(59,130,246,0.4);
}
.search-box input {
  background: transparent;
  color: #f1f5f9;
}
.product-item {
  background: rgba(30, 41, 59, 0.7);
  border-left: 4px solid #3b82f6;
  transition: 0.2s;
}
.product-item:hover {
  transform: translateX(6px);
  border-left-color: #10b981;
}
.product-price {
  color: #34d399;
  font-weight: bold;
}

解释

  • 搜索框毛玻璃风格,输入框透明,按钮渐变色。
  • 每个商品卡片带左侧蓝色条,hover 时右移并变为绿色。
  • 价格使用绿色高亮(#34d399),分类使用圆角标签。
  • 整体保持深色半透明背景,与全局主题一致。

🌐 五、API 演示页(ApiPage) – GET / POST 请求

10. ApiPage.jsx – 动态请求多种公开 API

// src/pages/ApiPage.jsx
import { useState, useEffect } from "react";
import "./ApiPage.css";

const apiList = [
  { id: 'posts', name: 'GET 示例数据 (JSONPlaceholder)', url: 'https://jsonplaceholder.typicode.com/posts', method: 'GET' },
  { id: 'countries', name: 'GET 国家信息 (REST Countries)', url: 'https://restcountries.com/v3.1/all', method: 'GET' },
  { id: 'spacex', name: 'GET SpaceX 最新发射', url: 'https://api.spacexdata.com/v5/launches/latest', method: 'GET' },
  { 
    id: 'post', 
    name: 'POST 创建新数据 (JSONPlaceholder)', 
    url: 'https://jsonplaceholder.typicode.com/posts', 
    method: 'POST',
    bodyData: { title: 'React Demo 商品', body: '这是一条通过 POST 请求创建的商品记录', userId: 1 }
  }
];

function ApiPage() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [selectedApi, setSelectedApi] = useState(apiList[0]);

  const fetchData = async () => {
    setLoading(true);
    setError(null);
    try {
      const { url, method, bodyData } = selectedApi;
      let response;
      if (method === 'GET') {
        response = await fetch(url);
      } else if (method === 'POST') {
        response = await fetch(url, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(bodyData),
        });
      }
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const result = await response.json();
      const formattedData = method === 'GET' && Array.isArray(result) ? result.slice(0, 5) : result;
      setData({ method, url, requestBody: method === 'POST' ? bodyData : null, response: formattedData });
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { fetchData(); }, [selectedApi]);

  return (
    <div className="api-page">
      <div className="api-container">
        <div className="api-header">
          <h2>📡 探索开放数据 (GET + POST)</h2>
          <p>从免费的公共 API 获取真实数据,并尝试向服务器发送新数据</p>
        </div>
        <div className="api-selector">
          <label>选择数据源:</label>
          <select value={selectedApi.id} onChange={(e) => setSelectedApi(apiList.find(api => api.id === e.target.value))}>
            {apiList.map(api => (<option key={api.id} value={api.id}>{api.method} – {api.name}</option>))}
          </select>
          <button onClick={fetchData} disabled={loading}>{loading ? '加载中...' : '重新获取'}</button>
        </div>
        <div className="api-result">
          {loading && <div className="loading-state"><div className="spinner"></div><p>正在执行 {selectedApi.method} 请求...</p></div>}
          {error && <div className="error-state"><p>❌ {error}</p><button onClick={fetchData}>重试</button></div>}
          {data && (
            <div className="data-display">
              <div className="data-meta">
                <span>请求方法: {data.method}</span>
                <span>请求地址: {data.url}</span>
              </div>
              {data.method === 'POST' && data.requestBody && (
                <div className="request-preview">
                  <div className="section-title">📤 发送的请求体 (JSON):</div>
                  <pre className="json-viewer request-body">{JSON.stringify(data.requestBody, null, 2)}</pre>
                </div>
              )}
              <div className="section-title">📥 服务器响应:</div>
              <pre className="json-viewer">{JSON.stringify(data.response, null, 2)}</pre>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default ApiPage;

解释

  • 定义 apiList 包含 GET(示例数据、国家信息、SpaceX 发射)和 POST(创建新数据)接口。
  • 使用 useState 管理数据、加载状态、错误以及当前选中的 API。
  • fetchData 根据 method 分别构造 fetch 请求,POST 时携带 JSON 体。
  • 对 GET 返回的数组只显示前 5 条,避免页面过长。
  • useEffect 监听 selectedApi 变化,自动请求新数据。
  • 界面包含下拉选择器、重新获取按钮,以及美观的 JSON 查看器(深色代码块)。
  • 错误处理:若请求失败,显示错误信息并提供重试按钮。

11. ApiPage.css – 响应式深色代码块

.api-page {
  min-height: 100vh;
  background: #0a0f1c;
  padding: 2rem;
}
.api-container {
  max-width: 1200px;
  margin: 0 auto;
  background: rgba(15, 23, 42, 0.85);
  backdrop-filter: blur(10px);
  border-radius: 28px;
  padding: 2rem;
  border: 1px solid rgba(59,130,246,0.3);
}
.json-viewer {
  background: #0f172a;
  padding: 1rem;
  border-radius: 16px;
  font-family: monospace;
  color: #e2e8f0;
  white-space: pre-wrap;
  word-break: break-word;
}
.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid rgba(59,130,246,0.3);
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

解释

  • 背景与全局一致,容器采用毛玻璃 + 大圆角。
  • JSON 数据使用深色代码块,等宽字体,自动换行。
  • 加载动画(spinner)用纯 CSS 实现旋转效果。
  • 响应式布局:在小屏幕上调整 padding 和选择器排列方向。

🧪 六、关键 React 概念总结

概念体现位置说明
函数组件所有 .jsx 文件React 推荐写法,配合 Hooks 实现状态与副作用
useStateSearch, ApiPage管理输入框值、搜索结果、加载状态等
useEffectApiPage监听 selectedApi 变化,自动发起网络请求
事件处理Search, ApiPageonClick, onChange, onKeyPress
条件渲染Search, ApiPage三元运算符 / && 控制加载、错误、结果区域的显示
列表渲染 & keySearchproducts.map() 生成商品列表,key 使用唯一 id
路由 (React Router)App.jsx, main.jsxBrowserRouter, Routes, Route, Link
Props 传递未显式使用(但可以通过扩展轻松加入)组件通信基础
网络请求 (fetch)ApiPageGET / POST 异步请求,处理响应与错误

🎨 七、样式设计亮点

  • 深色主题:全局背景 #0a0f1c,文字 #eef2ff,对比舒适。
  • 毛玻璃效果backdrop-filter: blur() 应用于导航栏、卡片、API 容器。
  • 蓝绿渐变点缀:标题、按钮、hover 状态使用 linear-gradient(135deg, #60a5fa, #34d399)
  • 微交互动画:按钮缩放、卡片平移、边框颜色切换,提升用户体验。
  • 响应式设计:移动端适配(@media)调整内边距和布局方向。

🚀 八、运行与扩展建议

运行项目

npm install
npm run dev

访问:

  • http://localhost:5173/ → 首页
  • /search → 商品搜索
  • /api → API 演示(默认 GET JSONPlaceholder)

扩展方向

  1. 添加更多 API:在 apiList 中增加其他公开接口(如天气、加密货币)。
  2. 实现用户自定义 POST 数据:添加表单输入框,动态构造 bodyData
  3. 加入状态管理:当项目变大时,引入 Redux 或 Zustand 管理全局用户偏好。
  4. 商品数据接入后端:将商品搜索改为调用真实接口,并增加分页功能。
  5. 表单处理库:使用 react-hook-form 优化搜索输入。

📝 结语

本文详细解析了一个完整的 React 演示项目,涵盖:

  • 路由与导航
  • 组件化开发与 Hooks 使用
  • 前端静态搜索
  • 调用第三方 API(GET / POST)
  • 现代化深色 UI 与交互动效

所有代码均已提供并附有行内注释与解释,你可以直接复制使用,也可以作为学习 React 的实践范例。如果有任何疑问或改进建议,欢迎在评论区留言!

Happy coding! ⚛️

更多推荐