06-高级模式与实战项目——19. 实战项目三:电商网站

📅 2026/7/9 1:35:15 👁️ 阅读次数 📝 编程学习
06-高级模式与实战项目——19. 实战项目三:电商网站

19. 实战项目三:电商网站

概述

电商网站是一个完整的全栈项目,涵盖商品展示、购物车、订单管理、用户认证等核心功能。通过这个项目,你将掌握复杂状态管理、购物车逻辑、支付集成等高级技能。

维度内容
What完整电商网站,包含商品列表、购物车、订单
Why掌握复杂状态管理、购物车逻辑
When高级实战练习
Where完整全栈项目
Who需要商业项目经验的开发者
How实现商品浏览、购物车、下单流程

1. 项目需求

1.1 功能列表

  • ✅ 商品列表展示(网格/列表视图)
  • ✅ 商品详情页面
  • ✅ 商品搜索/筛选/排序
  • ✅ 购物车(添加/删除/修改数量)
  • ✅ 用户认证(登录/注册)
  • ✅ 下单流程
  • ✅ 订单列表/详情
  • ✅ 用户个人中心
  • ✅ 支付集成(模拟)

1.2 技术栈

  • React 19
  • TypeScript
  • Zustand (状态管理)
  • React Router v6
  • Tailwind CSS
  • JSON Server (Mock API)

2. 项目结构

ecommerce-app/ ├── src/ │ ├── components/ │ │ ├── Layout/ │ │ ├── ProductCard.tsx │ │ ├── ProductGrid.tsx │ │ ├── CartItem.tsx │ │ ├── CartSummary.tsx │ │ ├── SearchBar.tsx │ │ ├── FilterSidebar.tsx │ │ └── Pagination.tsx │ ├── pages/ │ │ ├── HomePage.tsx │ │ ├── ProductsPage.tsx │ │ ├── ProductDetailPage.tsx │ │ ├── CartPage.tsx │ │ ├── CheckoutPage.tsx │ │ ├── OrdersPage.tsx │ │ ├── OrderDetailPage.tsx │ │ ├── LoginPage.tsx │ │ └── ProfilePage.tsx │ ├── stores/ │ │ ├── cartStore.ts │ │ ├── authStore.ts │ │ └── filterStore.ts │ ├── hooks/ │ │ ├── useProducts.ts │ │ ├── useProduct.ts │ │ └── useOrders.ts │ ├── api/ │ │ └── client.ts │ ├── types/ │ │ └── index.ts │ ├── utils/ │ │ └── format.ts │ ├── App.tsx │ └── main.tsx ├── db.json └── package.json

3. 代码实现

3.1 类型定义

// src/types/index.ts export interface Product { id: string; name: string; description: string; price: number; originalPrice?: number; images: string[]; category: string; tags: string[]; stock: number; rating: number; reviewCount: number; isNew?: boolean; isHot?: boolean; } export interface CartItem { product: Product; quantity: number; } export interface Order { id: string; userId: string; items: CartItem[]; totalAmount: number; status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled'; address: Address; paymentMethod: string; createdAt: string; } export interface Address { name: string; phone: string; province: string; city: string; district: string; detail: string; } export interface User { id: string; name: string; email: string; avatar?: string; }

3.2 Zustand 状态管理

// src/stores/cartStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { Product, CartItem } from '../types'; interface CartState { items: CartItem[]; addItem: (product: Product, quantity?: number) => void; removeItem: (productId: string) => void; updateQuantity: (productId: string, quantity: number) => void; clearCart: () => void; getTotalItems: () => number; getSubtotal: () => number; } export const useCartStore = create<CartState>()( persist( (set, get) => ({ items: [], addItem: (product, quantity = 1) => { const { items } = get(); const existingItem = items.find(item => item.product.id === product.id); if (existingItem) { set({ items: items.map(item => item.product.id === product.id ? { ...item, quantity: item.quantity + quantity } : item ), }); } else { set({ items: [...items, { product, quantity }] }); } }, removeItem: (productId) => { set({ items: get().items.filter(item => item.product.id !== productId) }); }, updateQuantity: (productId, quantity) => { if (quantity <= 0) { get().removeItem(productId); return; } set({ items: get().items.map(item => item.product.id === productId ? { ...item, quantity } : item ), }); }, clearCart: () => set({ items: [] }), getTotalItems: () => { return get().items.reduce((sum, item) => sum + item.quantity, 0); }, getSubtotal: () => { return get().items.reduce( (sum, item) => sum + item.product.price * item.quantity, 0 ); }, }), { name: 'cart-storage' } ) ); // src/stores/authStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { User } from '../types'; interface AuthState { user: User | null; token: string | null; login: (email: string, password: string) => Promise<void>; register: (name: string, email: string, password: string) => Promise<void>; logout: () => void; isLoading: boolean; error: string | null; } export const useAuthStore = create<AuthState>()( persist( (set) => ({ user: null, token: null, isLoading: false, error: null, login: async (email, password) => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const data = await response.json(); set({ user: data.user, token: data.token, isLoading: false }); } catch (error) { set({ error: '登录失败', isLoading: false }); } }, register: async (name, email, password) => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, password }), }); const data = await response.json(); set({ user: data.user, token: data.token, isLoading: false }); } catch (error) { set({ error: '注册失败', isLoading: false }); } }, logout: () => set({ user: null, token: null }), }), { name: 'auth-storage' } ) );

3.3 购物车组件

// src/components/CartItem.tsx import { CartItem as CartItemType } from '../types'; import { useCartStore } from '../stores/cartStore'; interface CartItemProps { item: CartItemType; } export default function CartItem({ item }: CartItemProps) { const { updateQuantity, removeItem } = useCartStore(); const { product, quantity } = item; return ( <div className="flex gap-4 py-4 border-b"> <img src={product.images[0]} alt={product.name} className="w-24 h-24 object-cover rounded" /> <div className="flex-1"> <h3 className="font-semibold">{product.name}</h3> <p className="text-gray-500 text-sm">{product.category}</p> <p className="text-blue-600 font-bold">¥{product.price}</p> </div> <div className="flex items-center gap-2"> <button onClick={() => updateQuantity(product.id, quantity - 1)} className="w-8 h-8 border rounded hover:bg-gray-100" > - </button> <span className="w-12 text-center">{quantity}</span> <button onClick={() => updateQuantity(product.id, quantity + 1)} className="w-8 h-8 border rounded hover:bg-gray-100" > + </button> <button onClick={() => removeItem(product.id)} className="ml-4 text-red-500 hover:text-red-700" > 删除 </button> </div> </div> ); } // src/components/CartSummary.tsx import { useCartStore } from '../stores/cartStore'; interface CartSummaryProps { onCheckout: () => void; } export default function CartSummary({ onCheckout }: CartSummaryProps) { const { getSubtotal, items } = useCartStore(); const subtotal = getSubtotal(); const shipping = subtotal > 200 ? 0 : 10; const total = subtotal + shipping; if (items.length === 0) return null; return ( <div className="bg-gray-50 p-6 rounded-lg"> <h3 className="text-lg font-bold mb-4">订单摘要</h3> <div className="space-y-2 mb-4"> <div className="flex justify-between"> <span>商品金额</span> <span>¥{subtotal.toFixed(2)}</span> </div> <div className="flex justify-between"> <span>运费</span> <span>{shipping === 0 ? '免运费' : `¥${shipping.toFixed(2)}`}</span> </div> <div className="border-t pt-2 mt-2"> <div className="flex justify-between font-bold text-lg"> <span>总计</span> <span className="text-blue-600">¥{total.toFixed(2)}</span> </div> </div> </div> <button onClick={onCheckout} className="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" > 去结算 </button> </div> ); }

3.4 商品组件

// src/components/ProductCard.tsx import { Link } from 'react-router-dom'; import { Product } from '../types'; import { useCartStore } from '../stores/cartStore'; interface ProductCardProps { product: Product; } export default function ProductCard({ product }: ProductCardProps) { const { addItem } = useCartStore(); const handleAddToCart = (e: React.MouseEvent) => { e.preventDefault(); addItem(product); }; return ( <Link to={`/products/${product.id}`} className="group"> <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"> <div className="relative"> <img src={product.images[0]} alt={product.name} className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300" /> {product.isNew && ( <span className="absolute top-2 left-2 px-2 py-1 bg-green-500 text-white text-xs rounded"> 新品 </span> )} {product.isHot && ( <span className="absolute top-2 left-2 px-2 py-1 bg-red-500 text-white text-xs rounded"> 热卖 </span> )} </div> <div className="p-4"> <h3 className="font-semibold text-lg mb-1 group-hover:text-blue-600"> {product.name} </h3> <p className="text-gray-500 text-sm mb-2">{product.category}</p> <div className="flex items-center gap-2 mb-3"> <span className="text-xl font-bold text-blue-600">¥{product.price}</span> {product.originalPrice && ( <span className="text-gray-400 line-through">¥{product.originalPrice}</span> )} </div> <div className="flex items-center justify-between"> <div className="flex items-center gap-1"> <span className="text-yellow-500">★</span> <span>{product.rating}</span> <span className="text-gray-400">({product.reviewCount})</span> </div> <button onClick={handleAddToCart} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" > 加入购物车 </button> </div> </div> </div> </Link> ); }

3.5 购物车页面

// src/pages/CartPage.tsx import { useNavigate } from 'react-router-dom'; import { useCartStore } from '../stores/cartStore'; import CartItem from '../components/CartItem'; import CartSummary from '../components/CartSummary'; export default function CartPage() { const navigate = useNavigate(); const { items, clearCart } = useCartStore(); const handleCheckout = () => { navigate('/checkout'); }; if (items.length === 0) { return ( <div className="text-center py-12"> <div className="text-6xl mb-4">🛒</div> <h2 className="text-2xl font-bold mb-2">购物车是空的</h2> <p className="text-gray-500 mb-6">去添加一些商品吧</p> <button onClick={() => navigate('/products')} className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" > 去购物 </button> </div> ); } return ( <div className="container mx-auto px-4 py-8"> <h1 className="text-3xl font-bold mb-8">购物车</h1> <div className="flex flex-col lg:flex-row gap-8"> <div className="flex-1"> {items.map(item => ( <CartItem key={item.product.id} item={item} /> ))} <div className="mt-4 flex justify-end"> <button onClick={clearCart} className="text-red-500 hover:text-red-700" > 清空购物车 </button> </div> </div> <div className="lg:w-80"> <CartSummary onCheckout={handleCheckout} /> </div> </div> </div> ); }

3.6 结算页面

// src/pages/CheckoutPage.tsx import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useCartStore } from '../stores/cartStore'; import { useAuthStore } from '../stores/authStore'; import { Address } from '../types'; export default function CheckoutPage() { const navigate = useNavigate(); const { user } = useAuthStore(); const { items, getSubtotal, clearCart } = useCartStore(); const [address, setAddress] = useState<Address>({ name: user?.name || '', phone: '', province: '', city: '', district: '', detail: '', }); const [paymentMethod, setPaymentMethod] = useState('wechat'); const [isSubmitting, setIsSubmitting] = useState(false); const subtotal = getSubtotal(); const shipping = subtotal > 200 ? 0 : 10; const total = subtotal + shipping; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); // 模拟下单 await new Promise(resolve => setTimeout(resolve, 1500)); // 创建订单 const order = { id: Date.now().toString(), userId: user?.id, items, totalAmount: total, status: 'pending', address, paymentMethod, createdAt: new Date().toISOString(), }; // 保存订单到 localStorage const orders = JSON.parse(localStorage.getItem('orders') || '[]'); orders.push(order); localStorage.setItem('orders', JSON.stringify(orders)); // 清空购物车 clearCart(); // 跳转到支付页面 navigate(`/orders/${order.id}/payment`); }; return ( <div className="container mx-auto px-4 py-8"> <h1 className="text-3xl font-bold mb-8">结算</h1> <div className="flex flex-col lg:flex-row gap-8"> <div className="flex-1"> <form onSubmit={handleSubmit} className="space-y-6"> {/* 收货信息 */} <div className="bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-bold mb-4">收货信息</h2> <div className="grid grid-cols-2 gap-4"> <div> <label className="block mb-1">姓名</label> <input value={address.name} onChange={(e) => setAddress({ ...address, name: e.target.value })} className="w-full border rounded p-2" required /> </div> <div> <label className="block mb-1">电话</label> <input value={address.phone} onChange={(e) => setAddress({ ...address, phone: e.target.value })} className="w-full border rounded p-2" required /> </div> <div> <label className="block mb-1">省份</label> <input value={address.province} onChange={(e) => setAddress({ ...address, province: e.target.value })} className="w-full border rounded p-2" /> </div> <div> <label className="block mb-1">城市</label> <input value={address.city} onChange={(e) => setAddress({ ...address, city: e.target.value })} className="w-full border rounded p-2" /> </div> <div className="col-span-2"> <label className="block mb-1">详细地址</label> <input value={address.detail} onChange={(e) => setAddress({ ...address, detail: e.target.value })} className="w-full border rounded p-2" /> </div> </div> </div> {/* 支付方式 */} <div className="bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-bold mb-4">支付方式</h2> <div className="space-y-2"> <label className="flex items-center gap-2"> <input type="radio" value="wechat" checked={paymentMethod === 'wechat'} onChange={(e) => setPaymentMethod(e.target.value)} /> <span>微信支付</span> </label> <label className="flex items-center gap-2"> <input type="radio" value="alipay" checked={paymentMethod === 'alipay'} onChange={(e) => setPaymentMethod(e.target.value)} /> <span>支付宝</span> </label> </div> </div> <button type="submit" disabled={isSubmitting} className="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" > {isSubmitting ? '提交中...' : `提交订单 ¥${total.toFixed(2)}`} </button> </form> </div> <div className="lg:w-80"> <CartSummary onCheckout={() => {}} /> </div> </div> </div> ); }

3.7 Mock 数据

// db.json{"products":[{"id":"1","name":"智能手机 Pro","description":"最新款智能手机,配备旗舰处理器和超清屏幕","price":5999,"originalPrice":6999,"images":["/images/phone-1.jpg"],"category":"电子产品","tags":["手机","旗舰"],"stock":100,"rating":4.8,"reviewCount":1234,"isNew":true}],"users":[{"id":"1","name":"张三","email":"zhang@example.com","password":"123456"}]}

4. 项目运行

# 安装依赖npminstallzustand react-router-dom# 启动项目npmrun dev

5. 总结

核心知识点

知识点应用
Zustand购物车状态、认证状态
购物车逻辑添加/删除/数量修改/金额计算
订单流程收货信息 → 支付 → 订单确认
持久化localStorage 保存购物车和登录状态