iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Modern Web

使用 React + Node.js + MongoDB 打造電商網站系列 第 11

Day11 商品詳細頁 + 前端新增商品

  • 分享至 

  • xImage
  •  

今天是鐵人賽 Day11,目標是完成商品詳細頁,能顯示商品圖片、價格、描述與庫存,並串接後端 API,讓使用者可以看到完整商品資訊;另外,也加上前端可以新增商品(含圖片)的功能。

1️⃣ 建立 ProductDetail 頁面與抓取單一商品 API
在 src/pages/ProductDetail.jsx 建立 ProductDetail component,利用 useEffect 根據 id 抓取後端 /api/products/:id 資料,並用 useState 管理商品、載入與錯誤狀態。

// ProductDetail.jsx
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";

export default function ProductDetail() {
  const { id } = useParams();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProduct = async () => {
      try {
        const res = await axios.get(`http://localhost:3000/api/products/${id}`);
        setProduct(res.data);
      } catch (err) {
        console.error("❌ API 錯誤:", err);
        setError("載入失敗,請稍後再試!");
      } finally {
        setLoading(false);
      }
    };

    fetchProduct();
  }, [id]);

  if (loading) return <p className="text-gray-500 text-center mt-10">載入中...</p>;
  if (error) return <p className="text-red-500 text-center mt-10">{error}</p>;
  if (!product) return <p className="text-gray-500 text-center mt-10">找不到商品</p>;

  return (
    <div className="p-6 min-h-screen bg-gray-50">
      <div className="max-w-2xl mx-auto bg-white shadow rounded-lg p-6">
        {product.image && (
          <img
            src={product.image ? `http://localhost:3000${product.image}` : '/default.jpg'}
            alt={product.name}
            className="w-full h-64 object-cover rounded mb-4"
          />
        )}
        <h1 className="text-2xl font-bold mb-2">{product.name}</h1>
        <p className="text-xl text-blue-600 mb-2">價格:${product.price}</p>
        <p className="text-gray-700 mb-4">{product.description}</p>
        <p className="text-sm text-gray-500 mb-4">
          {product.stock > 0 ? `庫存:${product.stock} 件` : "⚠️ 已售完"}
        </p>
        <button
          className="w-full bg-green-600 text-white py-2 rounded hover:bg-green-700 transition"
          disabled={product.stock === 0}
          onClick={() => alert(`已加入購物車:${product.name}`)}
        >
          {product.stock > 0 ? '加入購物車' : '已售完'}
        </button>
      </div>
    </div>
  );
}

https://ithelp.ithome.com.tw/upload/images/20250924/20178893YxGZxP77QW.png

2️⃣ 新增商品表單 (含圖片上傳)
在 src/pages/Products.jsx 新增表單,利用 FormData 上傳文字與圖片到後端 /api/products。

// Products.jsx
import { useState } from "react";
import axios from "axios";

export default function Products() {
  const [formData, setFormData] = useState({
    name: "",
    price: "",
    description: "",
    stock: ""
  });
  const [image, setImage] = useState(null);
  const [message, setMessage] = useState("");

  const handleChange = e => setFormData({ ...formData, [e.target.name]: e.target.value });
  const handleFileChange = e => setImage(e.target.files[0]);

  const handleSubmit = async e => {
    e.preventDefault();
    try {
      const data = new FormData();
      Object.keys(formData).forEach(key => data.append(key, formData[key]));
      if (image) data.append("image", image);

      const res = await axios.post("http://localhost:3000/api/products", data, {
        headers: { "Content-Type": "multipart/form-data" }
      });

      setMessage(`✅ 新增成功:${res.data.name}`);
      setFormData({ name: "", price: "", description: "", stock: "" });
      setImage(null);
    } catch (err) {
      console.error(err);
      setMessage("❌ 新增失敗");
    }
  };

  return (
    <div className="p-6 max-w-md mx-auto bg-white shadow rounded-lg">
      <h1 className="text-2xl font-bold mb-4">新增商品</h1>
      {message && <p className="mb-2">{message}</p>}
      <form onSubmit={handleSubmit} className="space-y-3">
        <input type="text" name="name" placeholder="商品名稱" value={formData.name} onChange={handleChange} className="w-full p-2 border rounded"/>
        <input type="number" name="price" placeholder="價格" value={formData.price} onChange={handleChange} className="w-full p-2 border rounded"/>
        <textarea name="description" placeholder="描述" value={formData.description} onChange={handleChange} className="w-full p-2 border rounded"/>
        <input type="number" name="stock" placeholder="庫存" value={formData.stock} onChange={handleChange} className="w-full p-2 border rounded"/>
        <input type="file" onChange={handleFileChange} className="w-full"/>
        <button type="submit" className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">新增商品</button>
      </form>
    </div>
  );
}

https://ithelp.ithome.com.tw/upload/images/20250924/20178893RiWn64T2fm.png

3️⃣ 前後端串接測試
後端:npx nodemon index.js,確認 /api/products 可接收 multipart/form-data。
前端:npm run dev,打開新增商品頁,填寫名稱、價格、描述、庫存,選擇圖片,按下「新增商品」,確認商品成功新增並可在首頁看到。
https://ithelp.ithome.com.tw/upload/images/20250924/20178893OeBh41TEqW.png

🐛 遇到的問題
圖片上傳失敗:後端需使用 multer,並設定 express.static('uploads')。
前端看不到圖片:前端要把 src 改成完整路徑 http://localhost:3000${product.image}。
CORS:前後端不同埠號要加上 cors()。

💡 Day11 收穫

  • 完成商品詳細頁,可顯示圖片、價格、描述、庫存
  • 新增前端商品表單,能上傳商品資訊與圖片
  • 學會 FormData 上傳檔案與文字資料
  • 處理載入中、錯誤與空資料狀態
  • Full Stack 前端 + 後端整合更加完整

上一篇
Day10 React 串接商品 API,首頁顯示商品清單
下一篇
Day 12 新增商品搜尋功能
系列文
使用 React + Node.js + MongoDB 打造電商網站12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言