Skip to content

购物车应用

学习目标

通过本章实战项目,你将综合运用:

  • 复杂State管理策略
  • 数组操作的最佳实践
  • 数据计算与派生状态
  • 组件组合与拆分
  • 性能优化技巧
  • 用户体验优化
  • 表单验证与处理
  • React 19新特性应用

项目概述

购物车是电商应用的核心功能,涵盖了React开发的多个重要概念:

  • 商品列表展示
  • 购物车状态管理
  • 数量增减控制
  • 价格计算
  • 优惠券处理
  • 本地存储
  • 动画交互

我们将从基础版本逐步完善到生产级应用。

第一部分:基础购物车

1.1 项目结构

shopping-cart/
├── components/
│   ├── ProductList.jsx
│   ├── ProductCard.jsx
│   ├── Cart.jsx
│   ├── CartItem.jsx
│   └── CartSummary.jsx
├── hooks/
│   ├── useCart.js
│   └── useLocalStorage.js
├── utils/
│   └── calculations.js
└── App.jsx

1.2 基础实现

jsx
import { useState, useMemo } from 'react';

function BasicShoppingCart() {
  // 商品数据
  const [products] = useState([
    { id: 1, name: 'iPhone 15 Pro', price: 7999, image: '/iphone.jpg', stock: 10 },
    { id: 2, name: 'MacBook Pro M3', price: 14999, image: '/macbook.jpg', stock: 5 },
    { id: 3, name: 'AirPods Pro', price: 1899, image: '/airpods.jpg', stock: 20 },
    { id: 4, name: 'iPad Air', price: 4799, image: '/ipad.jpg', stock: 15 },
    { id: 5, name: 'Apple Watch', price: 2999, image: '/watch.jpg', stock: 8 }
  ]);
  
  // 购物车数据
  const [cartItems, setCartItems] = useState([]);
  
  // 添加到购物车
  const addToCart = (product) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      
      if (existing) {
        // 已存在,增加数量
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      
      // 不存在,添加新项
      return [...prev, { ...product, quantity: 1 }];
    });
  };
  
  // 从购物车移除
  const removeFromCart = (productId) => {
    setCartItems(prev => prev.filter(item => item.id !== productId));
  };
  
  // 更新数量
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    setCartItems(prev =>
      prev.map(item =>
        item.id === productId ? { ...item, quantity } : item
      )
    );
  };
  
  // 清空购物车
  const clearCart = () => {
    setCartItems([]);
  };
  
  // 计算总价
  const total = useMemo(() => {
    return cartItems.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }, [cartItems]);
  
  return (
    <div className="shopping-cart-app">
      <header>
        <h1>购物车应用</h1>
        <div className="cart-badge">
          购物车 ({cartItems.length})
        </div>
      </header>
      
      <div className="main-content">
        {/* 商品列表 */}
        <section className="products-section">
          <h2>商品列表</h2>
          <div className="product-grid">
            {products.map(product => (
              <div key={product.id} className="product-card">
                <img src={product.image} alt={product.name} />
                <h3>{product.name}</h3>
                <p className="price">¥{product.price}</p>
                <p className="stock">库存: {product.stock}</p>
                <button
                  onClick={() => addToCart(product)}
                  disabled={product.stock === 0}
                  className="add-to-cart-btn"
                >
                  加入购物车
                </button>
              </div>
            ))}
          </div>
        </section>
        
        {/* 购物车 */}
        <section className="cart-section">
          <h2>购物车</h2>
          
          {cartItems.length === 0 ? (
            <div className="empty-cart">
              <p>购物车是空的</p>
              <p>快去选购商品吧!</p>
            </div>
          ) : (
            <>
              <div className="cart-items">
                {cartItems.map(item => (
                  <div key={item.id} className="cart-item">
                    <img src={item.image} alt={item.name} />
                    
                    <div className="item-info">
                      <h4>{item.name}</h4>
                      <p className="item-price">¥{item.price}</p>
                    </div>
                    
                    <div className="quantity-controls">
                      <button
                        onClick={() => updateQuantity(item.id, item.quantity - 1)}
                        className="qty-btn"
                      >
                        -
                      </button>
                      <input
                        type="number"
                        value={item.quantity}
                        onChange={(e) => updateQuantity(item.id, Number(e.target.value))}
                        min="1"
                        max={item.stock}
                        className="qty-input"
                      />
                      <button
                        onClick={() => updateQuantity(item.id, item.quantity + 1)}
                        disabled={item.quantity >= item.stock}
                        className="qty-btn"
                      >
                        +
                      </button>
                    </div>
                    
                    <div className="item-subtotal">
                      <p>小计</p>
                      <p className="subtotal-price">
                        ¥{(item.price * item.quantity).toFixed(2)}
                      </p>
                    </div>
                    
                    <button
                      onClick={() => removeFromCart(item.id)}
                      className="remove-btn"
                    >
                      删除
                    </button>
                  </div>
                ))}
              </div>
              
              <div className="cart-summary">
                <div className="summary-row">
                  <span>商品总价</span>
                  <span className="total-price">¥{total.toFixed(2)}</span>
                </div>
                
                <div className="cart-actions">
                  <button onClick={clearCart} className="clear-btn">
                    清空购物车
                  </button>
                  <button className="checkout-btn">
                    去结算
                  </button>
                </div>
              </div>
            </>
          )}
        </section>
      </div>
    </div>
  );
}

export default BasicShoppingCart;

第二部分:增强功能

2.1 添加优惠券功能

jsx
function CartWithCoupon() {
  const [products] = useState([/* ... */]);
  const [cartItems, setCartItems] = useState([]);
  const [couponCode, setCouponCode] = useState('');
  const [appliedCoupon, setAppliedCoupon] = useState(null);
  
  // 优惠券数据库
  const coupons = {
    'SAVE10': { type: 'percentage', value: 0.1, minAmount: 0 },
    'SAVE20': { type: 'percentage', value: 0.2, minAmount: 1000 },
    'SAVE100': { type: 'fixed', value: 100, minAmount: 500 },
    'FREESHIP': { type: 'shipping', value: 0, minAmount: 0 }
  };
  
  // 应用优惠券
  const applyCoupon = () => {
    const coupon = coupons[couponCode.toUpperCase()];
    
    if (coupon) {
      if (subtotal >= coupon.minAmount) {
        setAppliedCoupon({ code: couponCode, ...coupon });
      } else {
        alert(`此优惠券需要满 ¥${coupon.minAmount} 才能使用`);
      }
    } else {
      alert('优惠券无效');
    }
  };
  
  // 移除优惠券
  const removeCoupon = () => {
    setAppliedCoupon(null);
    setCouponCode('');
  };
  
  // 计算小计
  const subtotal = useMemo(() => {
    return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [cartItems]);
  
  // 计算优惠金额
  const discount = useMemo(() => {
    if (!appliedCoupon) return 0;
    
    if (appliedCoupon.type === 'percentage') {
      return subtotal * appliedCoupon.value;
    } else if (appliedCoupon.type === 'fixed') {
      return appliedCoupon.value;
    }
    
    return 0;
  }, [appliedCoupon, subtotal]);
  
  // 计算运费
  const shipping = useMemo(() => {
    if (appliedCoupon?.type === 'shipping') return 0;
    if (subtotal >= 299) return 0;
    return 20;
  }, [subtotal, appliedCoupon]);
  
  // 计算总价
  const total = subtotal - discount + shipping;
  
  return (
    <div className="shopping-cart">
      {/* 商品列表 */}
      <ProductList products={products} onAddToCart={addToCart} />
      
      {/* 购物车 */}
      <div className="cart">
        <CartItems items={cartItems} onUpdateQuantity={updateQuantity} onRemove={removeFromCart} />
        
        {/* 优惠券区域 */}
        <div className="coupon-section">
          <h3>优惠券</h3>
          <div className="coupon-input">
            <input
              type="text"
              value={couponCode}
              onChange={e => setCouponCode(e.target.value)}
              placeholder="输入优惠券代码"
              disabled={!!appliedCoupon}
            />
            {appliedCoupon ? (
              <button onClick={removeCoupon} className="remove-coupon-btn">
                移除
              </button>
            ) : (
              <button onClick={applyCoupon} className="apply-coupon-btn">
                应用
              </button>
            )}
          </div>
          
          {appliedCoupon && (
            <div className="applied-coupon">
              <span>已应用: {appliedCoupon.code}</span>
              <span className="discount-amount">
                -¥{discount.toFixed(2)}
              </span>
            </div>
          )}
          
          <div className="coupon-tips">
            <p>可用优惠券:</p>
            <ul>
              <li>SAVE10 - 9折优惠</li>
              <li>SAVE20 - 8折优惠(满¥1000)</li>
              <li>SAVE100 - 减¥100(满¥500)</li>
              <li>FREESHIP - 包邮</li>
            </ul>
          </div>
        </div>
        
        {/* 价格汇总 */}
        <div className="price-summary">
          <div className="summary-row">
            <span>商品小计</span>
            <span>¥{subtotal.toFixed(2)}</span>
          </div>
          
          {discount > 0 && (
            <div className="summary-row discount">
              <span>优惠</span>
              <span>-¥{discount.toFixed(2)}</span>
            </div>
          )}
          
          <div className="summary-row">
            <span>运费</span>
            <span>
              {shipping === 0 ? (
                <span className="free-shipping">免运费</span>
              ) : (
                `¥${shipping.toFixed(2)}`
              )}
            </span>
          </div>
          
          {subtotal > 0 && subtotal < 299 && shipping > 0 && (
            <div className="shipping-tip">
              再购买 ¥{(299 - subtotal).toFixed(2)} 即可包邮
            </div>
          )}
          
          <div className="summary-row total">
            <span>应付总额</span>
            <span className="total-price">¥{total.toFixed(2)}</span>
          </div>
        </div>
        
        <button className="checkout-btn" disabled={cartItems.length === 0}>
          去结算 ({cartItems.length})
        </button>
      </div>
    </div>
  );
}

2.2 添加收藏功能

jsx
function CartWithWishlist() {
  const [products] = useState([/* ... */]);
  const [cartItems, setCartItems] = useState([]);
  const [wishlist, setWishlist] = useState([]);
  
  // 添加到收藏
  const addToWishlist = (product) => {
    setWishlist(prev => {
      if (prev.find(item => item.id === product.id)) {
        return prev;  // 已存在
      }
      return [...prev, product];
    });
  };
  
  // 从收藏移除
  const removeFromWishlist = (productId) => {
    setWishlist(prev => prev.filter(item => item.id !== productId));
  };
  
  // 从收藏移到购物车
  const moveToCart = (product) => {
    addToCart(product);
    removeFromWishlist(product.id);
  };
  
  const isInWishlist = (productId) => {
    return wishlist.some(item => item.id === productId);
  };
  
  return (
    <div className="shopping-app">
      <nav>
        <span>购物车 ({cartItems.length})</span>
        <span>收藏 ({wishlist.length})</span>
      </nav>
      
      <ProductList
        products={products}
        onAddToCart={addToCart}
        onAddToWishlist={addToWishlist}
        isInWishlist={isInWishlist}
      />
      
      <Cart items={cartItems} />
      
      <Wishlist
        items={wishlist}
        onMoveToCart={moveToCart}
        onRemove={removeFromWishlist}
      />
    </div>
  );
}

2.3 添加搜索和筛选

jsx
function CartWithFilter() {
  const [products] = useState([/* ... */]);
  const [cartItems, setCartItems] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');
  const [priceRange, setPriceRange] = useState({ min: 0, max: Infinity });
  const [sortBy, setSortBy] = useState('default');
  
  // 过滤商品
  const filteredProducts = useMemo(() => {
    return products.filter(product => {
      // 搜索过滤
      const matchesSearch = product.name
        .toLowerCase()
        .includes(searchTerm.toLowerCase());
      
      // 分类过滤
      const matchesCategory = category === 'all' || product.category === category;
      
      // 价格过滤
      const matchesPrice = product.price >= priceRange.min && 
                          product.price <= priceRange.max;
      
      return matchesSearch && matchesCategory && matchesPrice;
    });
  }, [products, searchTerm, category, priceRange]);
  
  // 排序商品
  const sortedProducts = useMemo(() => {
    const sorted = [...filteredProducts];
    
    switch (sortBy) {
      case 'price-asc':
        return sorted.sort((a, b) => a.price - b.price);
      case 'price-desc':
        return sorted.sort((a, b) => b.price - a.price);
      case 'name':
        return sorted.sort((a, b) => a.name.localeCompare(b.name));
      default:
        return sorted;
    }
  }, [filteredProducts, sortBy]);
  
  return (
    <div className="shopping-cart">
      {/* 搜索和筛选栏 */}
      <div className="filter-bar">
        <input
          type="text"
          value={searchTerm}
          onChange={e => setSearchTerm(e.target.value)}
          placeholder="搜索商品..."
          className="search-input"
        />
        
        <select value={category} onChange={e => setCategory(e.target.value)}>
          <option value="all">全部分类</option>
          <option value="electronics">电子产品</option>
          <option value="accessories">配件</option>
          <option value="wearables">可穿戴</option>
        </select>
        
        <div className="price-filter">
          <input
            type="number"
            value={priceRange.min}
            onChange={e => setPriceRange(prev => ({ ...prev, min: Number(e.target.value) }))}
            placeholder="最低价"
          />
          <span>-</span>
          <input
            type="number"
            value={priceRange.max === Infinity ? '' : priceRange.max}
            onChange={e => setPriceRange(prev => ({ 
              ...prev, 
              max: e.target.value ? Number(e.target.value) : Infinity 
            }))}
            placeholder="最高价"
          />
        </div>
        
        <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
          <option value="default">默认排序</option>
          <option value="price-asc">价格升序</option>
          <option value="price-desc">价格降序</option>
          <option value="name">名称排序</option>
        </select>
      </div>
      
      <p className="result-count">
        找到 {sortedProducts.length} 个商品
      </p>
      
      <div className="product-grid">
        {sortedProducts.map(product => (
          <ProductCard
            key={product.id}
            product={product}
            onAddToCart={addToCart}
          />
        ))}
      </div>
      
      <Cart items={cartItems} />
    </div>
  );
}

第三部分:高级功能

3.1 本地存储持久化

jsx
// 自定义Hook: useLocalStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('读取localStorage失败:', error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('写入localStorage失败:', error);
    }
  };
  
  return [storedValue, setValue];
}

// 使用持久化
function PersistentCart() {
  const [products] = useState([/* ... */]);
  const [cartItems, setCartItems] = useLocalStorage('shopping-cart', []);
  const [wishlist, setWishlist] = useLocalStorage('wishlist', []);
  
  // 添加同步提示
  const [lastSaved, setLastSaved] = useState(null);
  
  useEffect(() => {
    setLastSaved(new Date());
  }, [cartItems, wishlist]);
  
  return (
    <div>
      {lastSaved && (
        <div className="save-indicator">
          最后保存: {lastSaved.toLocaleTimeString()}
        </div>
      )}
      
      <ProductList products={products} onAddToCart={addToCart} />
      <Cart items={cartItems} />
      <Wishlist items={wishlist} />
    </div>
  );
}

3.2 库存管理

jsx
function CartWithStockManagement() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Product 1', price: 100, stock: 10 },
    { id: 2, name: 'Product 2', price: 200, stock: 5 },
    { id: 3, name: 'Product 3', price: 300, stock: 0 }
  ]);
  
  const [cartItems, setCartItems] = useState([]);
  
  // 检查库存
  const checkStock = (productId, quantity) => {
    const product = products.find(p => p.id === productId);
    return product && product.stock >= quantity;
  };
  
  // 添加到购物车(检查库存)
  const addToCart = (product) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      const newQuantity = existing ? existing.quantity + 1 : 1;
      
      // 检查库存
      if (!checkStock(product.id, newQuantity)) {
        alert('库存不足');
        return prev;
      }
      
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      
      return [...prev, { ...product, quantity: 1 }];
    });
  };
  
  // 更新数量(检查库存)
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    if (!checkStock(productId, quantity)) {
      alert('库存不足');
      return;
    }
    
    setCartItems(prev =>
      prev.map(item =>
        item.id === productId ? { ...item, quantity } : item
      )
    );
  };
  
  // 获取可用库存
  const getAvailableStock = (productId) => {
    const product = products.find(p => p.id === productId);
    const cartItem = cartItems.find(item => item.id === productId);
    
    if (!product) return 0;
    
    return product.stock - (cartItem ? cartItem.quantity : 0);
  };
  
  return (
    <div>
      {/* 商品列表 */}
      {products.map(product => {
        const availableStock = getAvailableStock(product.id);
        const inCart = cartItems.find(item => item.id === product.id);
        
        return (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>¥{product.price}</p>
            <p>库存: {availableStock}</p>
            {inCart && <p className="in-cart">购物车中: {inCart.quantity}</p>}
            <button
              onClick={() => addToCart(product)}
              disabled={availableStock === 0}
            >
              {availableStock === 0 ? '缺货' : '加入购物车'}
            </button>
          </div>
        );
      })}
      
      {/* 购物车 */}
      <Cart
        items={cartItems}
        onUpdateQuantity={updateQuantity}
        getAvailableStock={getAvailableStock}
      />
    </div>
  );
}

3.3 结算流程

jsx
function CheckoutProcess() {
  const [cartItems, setCartItems] = useState([]);
  const [step, setStep] = useState(1);  // 1:购物车 2:地址 3:支付 4:完成
  const [shippingInfo, setShippingInfo] = useState({
    name: '',
    phone: '',
    address: '',
    city: '',
    zipCode: ''
  });
  const [paymentMethod, setPaymentMethod] = useState('alipay');
  
  const total = useMemo(() => {
    return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [cartItems]);
  
  // 验证配送信息
  const validateShipping = () => {
    const { name, phone, address, city, zipCode } = shippingInfo;
    
    if (!name || !phone || !address || !city || !zipCode) {
      alert('请填写完整的配送信息');
      return false;
    }
    
    if (!/^1[3-9]\d{9}$/.test(phone)) {
      alert('请输入正确的手机号');
      return false;
    }
    
    return true;
  };
  
  // 下一步
  const nextStep = () => {
    if (step === 1 && cartItems.length === 0) {
      alert('购物车是空的');
      return;
    }
    
    if (step === 2 && !validateShipping()) {
      return;
    }
    
    setStep(s => s + 1);
  };
  
  // 上一步
  const prevStep = () => {
    setStep(s => Math.max(1, s - 1));
  };
  
  // 提交订单
  const submitOrder = async () => {
    try {
      const order = {
        items: cartItems,
        shippingInfo,
        paymentMethod,
        total
      };
      
      // 模拟API调用
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('订单已提交:', order);
      
      // 清空购物车
      setCartItems([]);
      setStep(4);
    } catch (error) {
      alert('订单提交失败,请重试');
    }
  };
  
  return (
    <div className="checkout-process">
      {/* 步骤指示器 */}
      <div className="steps">
        <div className={`step ${step >= 1 ? 'active' : ''}`}>
          1. 购物车
        </div>
        <div className={`step ${step >= 2 ? 'active' : ''}`}>
          2. 配送信息
        </div>
        <div className={`step ${step >= 3 ? 'active' : ''}`}>
          3. 支付方式
        </div>
        <div className={`step ${step >= 4 ? 'active' : ''}`}>
          4. 完成
        </div>
      </div>
      
      {/* 步骤内容 */}
      <div className="step-content">
        {step === 1 && (
          <CartStep items={cartItems} onUpdateItems={setCartItems} />
        )}
        
        {step === 2 && (
          <ShippingStep
            shippingInfo={shippingInfo}
            onUpdate={setShippingInfo}
          />
        )}
        
        {step === 3 && (
          <PaymentStep
            paymentMethod={paymentMethod}
            onSelect={setPaymentMethod}
            total={total}
          />
        )}
        
        {step === 4 && (
          <CompletionStep />
        )}
      </div>
      
      {/* 导航按钮 */}
      <div className="navigation">
        {step > 1 && step < 4 && (
          <button onClick={prevStep} className="prev-btn">
            上一步
          </button>
        )}
        
        {step < 3 && (
          <button onClick={nextStep} className="next-btn">
            下一步
          </button>
        )}
        
        {step === 3 && (
          <button onClick={submitOrder} className="submit-btn">
            提交订单
          </button>
        )}
      </div>
    </div>
  );
}

// 配送信息步骤
function ShippingStep({ shippingInfo, onUpdate }) {
  const handleChange = (field) => (e) => {
    onUpdate(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  return (
    <div className="shipping-form">
      <h2>配送信息</h2>
      
      <div className="form-group">
        <label>收货人</label>
        <input
          value={shippingInfo.name}
          onChange={handleChange('name')}
          placeholder="请输入收货人姓名"
        />
      </div>
      
      <div className="form-group">
        <label>手机号</label>
        <input
          value={shippingInfo.phone}
          onChange={handleChange('phone')}
          placeholder="请输入手机号"
        />
      </div>
      
      <div className="form-group">
        <label>详细地址</label>
        <textarea
          value={shippingInfo.address}
          onChange={handleChange('address')}
          placeholder="请输入详细地址"
          rows={3}
        />
      </div>
      
      <div className="form-row">
        <div className="form-group">
          <label>城市</label>
          <input
            value={shippingInfo.city}
            onChange={handleChange('city')}
            placeholder="城市"
          />
        </div>
        
        <div className="form-group">
          <label>邮编</label>
          <input
            value={shippingInfo.zipCode}
            onChange={handleChange('zipCode')}
            placeholder="邮编"
          />
        </div>
      </div>
    </div>
  );
}

// 支付方式步骤
function PaymentStep({ paymentMethod, onSelect, total }) {
  const paymentMethods = [
    { id: 'alipay', name: '支付宝', icon: '💰' },
    { id: 'wechat', name: '微信支付', icon: '💚' },
    { id: 'card', name: '银行卡', icon: '💳' }
  ];
  
  return (
    <div className="payment-step">
      <h2>选择支付方式</h2>
      
      <div className="payment-methods">
        {paymentMethods.map(method => (
          <div
            key={method.id}
            className={`payment-method ${paymentMethod === method.id ? 'selected' : ''}`}
            onClick={() => onSelect(method.id)}
          >
            <span className="icon">{method.icon}</span>
            <span className="name">{method.name}</span>
            {paymentMethod === method.id && <span className="check">✓</span>}
          </div>
        ))}
      </div>
      
      <div className="payment-summary">
        <h3>订单金额</h3>
        <p className="total">¥{total.toFixed(2)}</p>
      </div>
    </div>
  );
}

// 完成步骤
function CompletionStep() {
  return (
    <div className="completion-step">
      <div className="success-icon">✓</div>
      <h2>订单提交成功!</h2>
      <p>我们会尽快为您发货</p>
      <button className="continue-btn">
        继续购物
      </button>
    </div>
  );
}

第四部分:组件拆分

4.1 ProductCard组件

jsx
const ProductCard = React.memo(function ProductCard({ 
  product, 
  onAddToCart,
  onAddToWishlist,
  isInWishlist 
}) {
  const [isHovered, setIsHovered] = useState(false);
  
  const handleAddToCart = useCallback(() => {
    onAddToCart(product);
  }, [product, onAddToCart]);
  
  const handleWishlist = useCallback(() => {
    onAddToWishlist(product);
  }, [product, onAddToWishlist]);
  
  return (
    <div
      className="product-card"
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      <div className="product-image">
        <img src={product.image} alt={product.name} />
        
        {isHovered && (
          <div className="quick-view">
            <button className="wishlist-btn" onClick={handleWishlist}>
              {isInWishlist ? '❤️' : '🤍'}
            </button>
          </div>
        )}
        
        {product.stock === 0 && (
          <div className="out-of-stock">缺货</div>
        )}
        
        {product.discount && (
          <div className="discount-badge">
            -{product.discount}%
          </div>
        )}
      </div>
      
      <div className="product-info">
        <h3 className="product-name">{product.name}</h3>
        <p className="product-description">{product.description}</p>
        
        <div className="product-footer">
          <div className="price-section">
            {product.discount ? (
              <>
                <span className="original-price">
                  ¥{product.price}
                </span>
                <span className="sale-price">
                  ¥{(product.price * (1 - product.discount / 100)).toFixed(2)}
                </span>
              </>
            ) : (
              <span className="price">¥{product.price}</span>
            )}
          </div>
          
          <button
            onClick={handleAddToCart}
            disabled={product.stock === 0}
            className="add-to-cart-btn"
          >
            {product.stock === 0 ? '缺货' : '加入购物车'}
          </button>
        </div>
      </div>
    </div>
  );
});

export default ProductCard;

4.2 CartItem组件

jsx
const CartItem = React.memo(function CartItem({ 
  item, 
  onUpdateQuantity, 
  onRemove,
  getAvailableStock 
}) {
  const [quantity, setQuantity] = useState(item.quantity);
  const [isEditing, setIsEditing] = useState(false);
  
  // 立即更新数量
  const handleIncrement = useCallback(() => {
    const availableStock = getAvailableStock(item.id);
    if (quantity < item.stock && quantity < availableStock + item.quantity) {
      const newQty = quantity + 1;
      setQuantity(newQty);
      onUpdateQuantity(item.id, newQty);
    }
  }, [item.id, item.stock, quantity, getAvailableStock, onUpdateQuantity]);
  
  const handleDecrement = useCallback(() => {
    if (quantity > 1) {
      const newQty = quantity - 1;
      setQuantity(newQty);
      onUpdateQuantity(item.id, newQty);
    }
  }, [item.id, quantity, onUpdateQuantity]);
  
  // 手动输入数量
  const handleInputChange = (e) => {
    const value = parseInt(e.target.value) || 1;
    setQuantity(value);
  };
  
  const handleInputBlur = () => {
    const validQuantity = Math.max(1, Math.min(quantity, item.stock));
    setQuantity(validQuantity);
    onUpdateQuantity(item.id, validQuantity);
    setIsEditing(false);
  };
  
  const handleRemove = useCallback(() => {
    onRemove(item.id);
  }, [item.id, onRemove]);
  
  const subtotal = item.price * item.quantity;
  
  return (
    <div className="cart-item">
      <div className="item-image">
        <img src={item.image} alt={item.name} />
      </div>
      
      <div className="item-details">
        <h4>{item.name}</h4>
        <p className="item-specs">{item.specs}</p>
        <p className="unit-price">单价: ¥{item.price}</p>
      </div>
      
      <div className="quantity-control">
        <button
          onClick={handleDecrement}
          disabled={quantity <= 1}
          className="qty-btn"
        >
          -
        </button>
        
        {isEditing ? (
          <input
            type="number"
            value={quantity}
            onChange={handleInputChange}
            onBlur={handleInputBlur}
            onKeyPress={e => e.key === 'Enter' && handleInputBlur()}
            autoFocus
            className="qty-input-edit"
          />
        ) : (
          <span
            className="qty-display"
            onClick={() => setIsEditing(true)}
          >
            {quantity}
          </span>
        )}
        
        <button
          onClick={handleIncrement}
          disabled={quantity >= item.stock}
          className="qty-btn"
        >
          +
        </button>
      </div>
      
      <div className="item-subtotal">
        <p className="subtotal-label">小计</p>
        <p className="subtotal-price">¥{subtotal.toFixed(2)}</p>
      </div>
      
      <button onClick={handleRemove} className="remove-btn">
        删除
      </button>
    </div>
  );
});

export default CartItem;

4.3 CartSummary组件

jsx
const CartSummary = React.memo(function CartSummary({ 
  subtotal, 
  discount, 
  shipping, 
  total,
  onCheckout,
  onClearCart 
}) {
  return (
    <div className="cart-summary">
      <h3>价格明细</h3>
      
      <div className="summary-details">
        <div className="summary-row">
          <span>商品金额</span>
          <span>¥{subtotal.toFixed(2)}</span>
        </div>
        
        {discount > 0 && (
          <div className="summary-row discount-row">
            <span>优惠金额</span>
            <span className="discount-amount">-¥{discount.toFixed(2)}</span>
          </div>
        )}
        
        <div className="summary-row">
          <span>运费</span>
          <span>
            {shipping === 0 ? (
              <span className="free-shipping">包邮</span>
            ) : (
              `¥${shipping.toFixed(2)}`
            )}
          </span>
        </div>
        
        <div className="summary-divider"></div>
        
        <div className="summary-row total-row">
          <span>应付总额</span>
          <span className="total-amount">¥{total.toFixed(2)}</span>
        </div>
      </div>
      
      <div className="summary-actions">
        <button onClick={onClearCart} className="clear-cart-btn">
          清空购物车
        </button>
        <button onClick={onCheckout} className="checkout-btn">
          去结算
        </button>
      </div>
    </div>
  );
});

export default CartSummary;

第五部分:性能优化

5.1 使用useCallback和memo

jsx
function OptimizedCart() {
  const [products] = useState([/* ... */]);
  const [cartItems, setCartItems] = useState([]);
  
  // 缓存所有回调函数
  const addToCart = useCallback((product) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  }, []);
  
  const updateQuantity = useCallback((id, quantity) => {
    setCartItems(prev =>
      prev.map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    );
  }, []);
  
  const removeItem = useCallback((id) => {
    setCartItems(prev => prev.filter(item => item.id !== id));
  }, []);
  
  const clearCart = useCallback(() => {
    setCartItems([]);
  }, []);
  
  // 缓存计算结果
  const totals = useMemo(() => {
    const subtotal = cartItems.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    const shipping = subtotal >= 299 ? 0 : 20;
    const total = subtotal + shipping;
    
    return { subtotal, shipping, total };
  }, [cartItems]);
  
  return (
    <div>
      <MemoProductList products={products} onAddToCart={addToCart} />
      <MemoCart
        items={cartItems}
        onUpdateQuantity={updateQuantity}
        onRemove={removeItem}
        onClear={clearCart}
        totals={totals}
      />
    </div>
  );
}

const MemoProductList = React.memo(function ProductList({ products, onAddToCart }) {
  console.log('ProductList渲染');
  
  return (
    <div className="product-grid">
      {products.map(product => (
        <MemoProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
});

const MemoProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard渲染:', product.id);
  
  const handleClick = useCallback(() => {
    onAddToCart(product);
  }, [product, onAddToCart]);
  
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
      <button onClick={handleClick}>加入购物车</button>
    </div>
  );
});

const MemoCart = React.memo(function Cart({ 
  items, 
  onUpdateQuantity, 
  onRemove, 
  onClear,
  totals 
}) {
  console.log('Cart渲染');
  
  return (
    <div className="cart">
      {items.map(item => (
        <MemoCartItem
          key={item.id}
          item={item}
          onUpdateQuantity={onUpdateQuantity}
          onRemove={onRemove}
        />
      ))}
      
      <CartSummary
        subtotal={totals.subtotal}
        shipping={totals.shipping}
        total={totals.total}
        onClear={onClear}
      />
    </div>
  );
});

const MemoCartItem = React.memo(function CartItem({ item, onUpdateQuantity, onRemove }) {
  console.log('CartItem渲染:', item.id);
  
  const handleUpdate = useCallback((newQuantity) => {
    onUpdateQuantity(item.id, newQuantity);
  }, [item.id, onUpdateQuantity]);
  
  const handleRemove = useCallback(() => {
    onRemove(item.id);
  }, [item.id, onRemove]);
  
  return (
    <div className="cart-item">
      <img src={item.image} alt={item.name} />
      <div className="item-info">
        <h4>{item.name}</h4>
        <p>¥{item.price}</p>
      </div>
      <div className="quantity">
        <button onClick={() => handleUpdate(item.quantity - 1)}>-</button>
        <span>{item.quantity}</span>
        <button onClick={() => handleUpdate(item.quantity + 1)}>+</button>
      </div>
      <div className="subtotal">
        ¥{(item.price * item.quantity).toFixed(2)}
      </div>
      <button onClick={handleRemove}>删除</button>
    </div>
  );
});

5.2 虚拟滚动优化

jsx
import { FixedSizeList } from 'react-window';

function VirtualizedProductList({ products, onAddToCart }) {
  const Row = useCallback(({ index, style }) => {
    const product = products[index];
    
    return (
      <div style={style}>
        <MemoProductCard
          product={product}
          onAddToCart={onAddToCart}
        />
      </div>
    );
  }, [products, onAddToCart]);
  
  return (
    <FixedSizeList
      height={800}
      itemCount={products.length}
      itemSize={250}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

第六部分:用户体验优化

6.1 加载状态

jsx
function CartWithLoading() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetchProducts()
      .then(data => {
        setProducts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);
  
  if (loading) {
    return (
      <div className="loading-state">
        <div className="spinner"></div>
        <p>加载中...</p>
      </div>
    );
  }
  
  if (error) {
    return (
      <div className="error-state">
        <p>加载失败: {error}</p>
        <button onClick={() => window.location.reload()}>
          重试
        </button>
      </div>
    );
  }
  
  return <ShoppingCart products={products} />;
}

6.2 添加动画效果

jsx
import { useTransition, animated } from 'react-spring';

function AnimatedCart() {
  const [cartItems, setCartItems] = useState([]);
  
  // 列表动画
  const transitions = useTransition(cartItems, {
    keys: item => item.id,
    from: { opacity: 0, transform: 'translateX(100%)' },
    enter: { opacity: 1, transform: 'translateX(0%)' },
    leave: { opacity: 0, transform: 'translateX(-100%)' }
  });
  
  return (
    <div className="cart">
      {transitions((style, item) => (
        <animated.div style={style}>
          <CartItem item={item} />
        </animated.div>
      ))}
    </div>
  );
}

6.3 添加提示信息

jsx
function CartWithToast() {
  const [cartItems, setCartItems] = useState([]);
  const [toast, setToast] = useState(null);
  
  // 显示提示
  const showToast = (message, type = 'success') => {
    setToast({ message, type });
    setTimeout(() => setToast(null), 3000);
  };
  
  // 添加到购物车(带提示)
  const addToCart = (product) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      
      if (existing) {
        showToast(`${product.name} 数量已增加`);
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      
      showToast(`${product.name} 已加入购物车`, 'success');
      return [...prev, { ...product, quantity: 1 }];
    });
  };
  
  // 删除商品(带确认)
  const removeFromCart = (productId) => {
    const item = cartItems.find(i => i.id === productId);
    
    if (confirm(`确定要删除 ${item.name} 吗?`)) {
      setCartItems(prev => prev.filter(i => i.id !== productId));
      showToast(`${item.name} 已从购物车移除`, 'info');
    }
  };
  
  return (
    <div>
      {toast && (
        <div className={`toast toast-${toast.type}`}>
          {toast.message}
        </div>
      )}
      
      <ProductList products={products} onAddToCart={addToCart} />
      <Cart items={cartItems} onRemove={removeFromCart} />
    </div>
  );
}

第七部分:React 19版本

7.1 使用Server Actions

jsx
// app/actions/cart.js
'use server';

import { revalidatePath } from 'next/cache';

export async function addToCart(userId, productId, quantity = 1) {
  await db.cart.upsert({
    where: { userId_productId: { userId, productId } },
    update: { quantity: { increment: quantity } },
    create: { userId, productId, quantity }
  });
  
  revalidatePath('/cart');
  return { success: true };
}

export async function updateCartItem(userId, productId, quantity) {
  if (quantity <= 0) {
    await db.cart.delete({
      where: { userId_productId: { userId, productId } }
    });
  } else {
    await db.cart.update({
      where: { userId_productId: { userId, productId } },
      data: { quantity }
    });
  }
  
  revalidatePath('/cart');
  return { success: true };
}

export async function clearCart(userId) {
  await db.cart.deleteMany({
    where: { userId }
  });
  
  revalidatePath('/cart');
  return { success: true };
}

// app/components/Cart.jsx
'use client';

import { useOptimistic, useTransition } from 'react';
import { addToCart, updateCartItem, clearCart } from '../actions/cart';

function ServerCart({ userId, initialItems }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticItems, updateOptimistic] = useOptimistic(
    initialItems,
    (state, { action, productId, quantity, product }) => {
      switch (action) {
        case 'add':
          const existing = state.find(item => item.id === productId);
          if (existing) {
            return state.map(item =>
              item.id === productId
                ? { ...item, quantity: item.quantity + 1 }
                : item
            );
          }
          return [...state, { ...product, quantity: 1 }];
          
        case 'update':
          if (quantity <= 0) {
            return state.filter(item => item.id !== productId);
          }
          return state.map(item =>
            item.id === productId ? { ...item, quantity } : item
          );
          
        case 'clear':
          return [];
          
        default:
          return state;
      }
    }
  );
  
  const handleAddToCart = async (product) => {
    startTransition(async () => {
      updateOptimistic({ action: 'add', productId: product.id, product });
      await addToCart(userId, product.id);
    });
  };
  
  const handleUpdateQuantity = async (productId, quantity) => {
    startTransition(async () => {
      updateOptimistic({ action: 'update', productId, quantity });
      await updateCartItem(userId, productId, quantity);
    });
  };
  
  const handleClearCart = async () => {
    startTransition(async () => {
      updateOptimistic({ action: 'clear' });
      await clearCart(userId);
    });
  };
  
  return (
    <div className="cart">
      {isPending && <div className="loading-overlay">处理中...</div>}
      
      <CartItems
        items={optimisticItems}
        onUpdateQuantity={handleUpdateQuantity}
      />
      
      <button onClick={handleClearCart} disabled={isPending}>
        清空购物车
      </button>
    </div>
  );
}

7.2 使用useActionState

jsx
'use client';

import { useActionState } from 'react';
import { checkout } from '../actions/checkout';

function CheckoutForm({ cartItems }) {
  const [state, formAction, isPending] = useActionState(checkout, null);
  
  return (
    <form action={formAction}>
      <h2>结算</h2>
      
      <div className="order-summary">
        {cartItems.map(item => (
          <div key={item.id}>
            <span>{item.name} x {item.quantity}</span>
            <span>¥{(item.price * item.quantity).toFixed(2)}</span>
          </div>
        ))}
      </div>
      
      <div className="shipping-form">
        <input name="name" placeholder="收货人" required />
        <input name="phone" placeholder="手机号" required />
        <textarea name="address" placeholder="详细地址" required />
      </div>
      
      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '提交订单'}
      </button>
      
      {state?.success && (
        <div className="success-message">
          订单提交成功!订单号: {state.orderId}
        </div>
      )}
      
      {state?.error && (
        <div className="error-message">
          {state.error}
        </div>
      )}
    </form>
  );
}

第八部分:完整样式

css
/* ShoppingCart.css */
.shopping-cart-app {
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 0;
  border-bottom: 2px solid #eee;
}

.cart-badge {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border-radius: 20px;
}

.main-content {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 30px;
  margin-top: 30px;
}

/* 商品列表 */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  transition: transform 0.2s, box-shadow 0.2s;
  position: relative;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.product-image {
  position: relative;
  width: 100%;
  height: 200px;
  overflow: hidden;
  border-radius: 4px;
  margin-bottom: 15px;
}

.product-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.discount-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  background: #e74c3c;
  color: white;
  padding: 5px 10px;
  border-radius: 4px;
  font-weight: bold;
}

.out-of-stock {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
}

.product-name {
  font-size: 16px;
  margin: 10px 0;
  font-weight: 600;
}

.price {
  font-size: 24px;
  color: #e74c3c;
  font-weight: bold;
  margin: 10px 0;
}

.add-to-cart-btn {
  width: 100%;
  padding: 12px;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background 0.2s;
}

.add-to-cart-btn:hover {
  background: #218838;
}

.add-to-cart-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

/* 购物车 */
.cart-section {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  position: sticky;
  top: 20px;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.empty-cart {
  text-align: center;
  padding: 60px 20px;
  color: #999;
}

.cart-items {
  margin-bottom: 20px;
}

.cart-item {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 15px;
  background: white;
  border-radius: 8px;
  margin-bottom: 15px;
}

.cart-item img {
  width: 80px;
  height: 80px;
  object-fit: cover;
  border-radius: 4px;
}

.item-info {
  flex: 1;
}

.item-info h4 {
  margin: 0 0 5px;
  font-size: 16px;
}

.item-price {
  color: #e74c3c;
  font-weight: bold;
}

.quantity-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.qty-btn {
  width: 30px;
  height: 30px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
  font-size: 18px;
}

.qty-btn:hover {
  background: #f0f0f0;
}

.qty-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.qty-input {
  width: 50px;
  text-align: center;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 5px;
}

.item-subtotal {
  text-align: right;
  min-width: 100px;
}

.subtotal-price {
  font-weight: bold;
  font-size: 18px;
  color: #e74c3c;
}

.remove-btn {
  padding: 8px 16px;
  background: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

/* 价格汇总 */
.price-summary {
  background: white;
  padding: 20px;
  border-radius: 8px;
  margin-top: 20px;
}

.summary-row {
  display: flex;
  justify-content: space-between;
  padding: 10px 0;
}

.summary-row.discount-row {
  color: #28a745;
}

.summary-row.total-row {
  font-size: 20px;
  font-weight: bold;
  border-top: 2px solid #eee;
  margin-top: 10px;
  padding-top: 15px;
}

.total-price {
  color: #e74c3c;
}

.free-shipping {
  color: #28a745;
  font-weight: bold;
}

.shipping-tip {
  background: #fff3cd;
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
  text-align: center;
  font-size: 14px;
}

/* 操作按钮 */
.cart-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.clear-cart-btn {
  flex: 1;
  padding: 15px;
  background: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.checkout-btn {
  flex: 2;
  padding: 15px;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 18px;
  font-weight: bold;
  cursor: pointer;
}

.checkout-btn:hover {
  background: #218838;
}

.checkout-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

/* 优惠券 */
.coupon-section {
  background: white;
  padding: 20px;
  border-radius: 8px;
  margin: 20px 0;
}

.coupon-input {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.coupon-input input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.apply-coupon-btn {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.applied-coupon {
  display: flex;
  justify-content: space-between;
  background: #d4edda;
  padding: 10px;
  border-radius: 4px;
  color: #155724;
}

.discount-amount {
  font-weight: bold;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .main-content {
    grid-template-columns: 1fr;
  }
  
  .cart-section {
    position: static;
  }
  
  .product-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }
}

练习题

基础练习

  1. 实现基础购物车的增删改功能
  2. 添加商品数量控制
  3. 计算并显示总价

进阶练习

  1. 实现优惠券功能
  2. 添加库存管理
  3. 实现收藏列表
  4. 添加搜索和筛选功能

高级练习

  1. 实现完整的结算流程
  2. 使用本地存储持久化购物车
  3. 添加动画效果提升用户体验
  4. 使用React 19的Server Actions实现服务端购物车
  5. 性能优化:使用memo系列Hook和虚拟滚动

总结

通过完成购物车应用,你已经:

  • 掌握了复杂State的管理策略
  • 学会了数组操作的各种技巧
  • 理解了数据计算与派生状态
  • 掌握了组件拆分和复用
  • 学会了性能优化方法
  • 提升了用户体验设计能力
  • 体验了React 19的新特性

购物车是电商应用的核心,也是React开发的综合实践。恭喜你完成了Part1的所有学习!你已经掌握了React核心基础,可以开始构建实际的生产级应用了!

继续学习Part2,深入掌握React Hooks的强大功能!