第七章:项目实战 - 第三节 - Tailwind CSS 电商网站开发

发布于:2025-03-06 ⋅ 阅读:(171) ⋅ 点赞:(0)

本节将介绍如何使用 Tailwind CSS 开发一个现代化的电商网站,包括商品展示、购物车、结算流程等核心功能的实现。

商品列表

商品卡片组件

// components/ProductCard.tsx
interface ProductCardProps {
  product: {
    id: string;
    title: string;
    price: number;
    image: string;
    discount?: number;
    tags?: string[];
  };
  onAddToCart: (productId: string) => void;
}

const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
  return (
    <div className="group relative">
      {/* 商品图片 */}
      <div className="aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-200">
        <img
          src={product.image}
          alt={product.title}
          className="h-full w-full object-cover object-center group-hover:opacity-75 transition-opacity"
        />
        {product.discount && (
          <div className="absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded-md text-sm font-medium">
            -{product.discount}%
          </div>
        )}
      </div>

      {/* 商品信息 */}
      <div className="mt-4 flex justify-between">
        <div>
          <h3 className="text-sm text-gray-700">
            <a href={`/product/${product.id}`}>
              <span aria-hidden="true" className="absolute inset-0" />
              {product.title}
            </a>
          </h3>
          <div className="mt-1 flex items-center space-x-2">
            <p className="text-lg font-medium text-gray-900">
              ¥{product.price}
            </p>
            {product.discount && (
              <p className="text-sm text-gray-500 line-through">
                ¥{(product.price * (100 + product.discount) / 100).toFixed(2)}
              </p>
            )}
          </div>
        </div>
        <button
          onClick={() => onAddToCart(product.id)}
          className="inline-flex items-center p-2 rounded-full bg-blue-500 text-white shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
        >
          <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
          </svg>
        </button>
      </div>

      {/* 商品标签 */}
      {product.tags && product.tags.length > 0 && (
        <div className="mt-2 flex flex-wrap gap-1">
          {product.tags.map(tag => (
            <span
              key={tag}
              className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800"
            >
              {tag}
            </span>
          ))}
        </div>
      )}
    </div>
  );
};

商品列表页面

// pages/ProductList.tsx
import { useState } from 'react';
import ProductCard from '../components/ProductCard';
import { useCart } from '../hooks/useCart';

const filters = [
  { id: 'category', name: '分类', options: ['全部', '电子产品', '服装', '食品'] },
  { id: 'price', name: '价格', options: ['全部', '0-100', '100-500', '500+'] },
  // ... 更多筛选选项
];

const ProductList = () => {
  const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
  const { addToCart } = useCart();

  return (
    <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      {/* 筛选器 */}
      <div className="py-4 border-b border-gray-200">
        <div className="flex flex-wrap gap-4">
          {filters.map(filter => (
            <div key={filter.id} className="relative">
              <select
                className="appearance-none bg-white border border-gray-300 rounded-md py-2 pl-3 pr-8 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
                value={activeFilters[filter.id] || ''}
                onChange={(e) => {
                  setActiveFilters(prev => ({
                    ...prev,
                    [filter.id]: e.target.value
                  }));
                }}
              >
                <option value="">{filter.name}</option>
                {filter.options.map(option => (
                  <option key={option} value={option}>
                    {option}
                  </option>
                ))}
              </select>
              <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
                <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
                  <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
                </svg>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* 商品网格 */}
      <div className="mt-6 grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-4">
        {products.map(product => (
          <ProductCard
            key={product.id}
            product={product}
            onAddToCart={addToCart}
          />
        ))}
      </div>

      {/* 分页 */}
      <div className="mt-8 flex justify-center">
        <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
          <a
            href="#"
            className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
          >
            上一页
          </a>
          {/* 页码 */}
          <a
            href="#"
            className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
          >
            1
          </a>
          <a
            href="#"
            className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
          >
            下一页
          </a>
        </nav>
      </div>
    </div>
  );
};

购物车功能

购物车 Hook

// hooks/useCart.ts
import { useState, useCallback } from 'react';

interface CartItem {
  id: string;
  quantity: number;
  price: number;
  title: string;
  image: string;
}

export const useCart = () => {
  const [items, setItems] = useState<CartItem[]>([]);

  const addToCart = useCallback((product: Omit<CartItem, 'quantity'>) => {
    setItems(prev => {
      const existingItem = prev.find(item => item.id === product.id);
      
      if (existingItem) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }

      return [...prev, { ...product, quantity: 1 }];
    });
  }, []);

  const removeFromCart = useCallback((productId: string) => {
    setItems(prev => prev.filter(item => item.id !== productId));
  }, []);

  const updateQuantity = useCallback((productId: string, quantity: number) => {
    setItems(prev =>
      prev.map(item =>
        item.id === productId
          ? { ...item, quantity: Math.max(0, quantity) }
          : item
      ).filter(item => item.quantity > 0)
    );
  }, []);

  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return {
    items,
    addToCart,
    removeFromCart,
    updateQuantity,
    total
  };
};

购物车组件

// components/Cart.tsx
import { useCart } from '../hooks/useCart';

const Cart = () => {
  const { items, removeFromCart, updateQuantity, total } = useCart();

  return (
    <div className="fixed inset-y-0 right-0 w-96 bg-white shadow-xl">
      <div className="flex flex-col h-full">
        {/* 购物车头部 */}
        <div className="px-4 py-6 bg-gray-50">
          <h2 className="text-lg font-medium text-gray-900">购物车</h2>
        </div>

        {/* 购物车列表 */}
        <div className="flex-1 overflow-y-auto py-6 px-4">
          {items.length === 0 ? (
            <div className="text-center py-12">
              <svg
                className="mx-auto h-12 w-12 text-gray-400"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
                />
              </svg>
              <p className="mt-4 text-sm text-gray-500">
                购物车是空的
              </p>
            </div>
          ) : (
            <div className="space-y-6">
              {items.map(item => (
                <div key={item.id} className="flex">
                  <img
                    src={item.image}
                    alt={item.title}
                    className="h-20 w-20 flex-shrink-0 rounded-md object-cover"
                  />
                  <div className="ml-4 flex flex-1 flex-col">
                    <div>
                      <div className="flex justify-between text-base font-medium text-gray-900">
                        <h3>{item.title}</h3>
                        <p className="ml-4">¥{item.price}</p>
                      </div>
                    </div>
                    <div className="flex flex-1 items-end justify-between text-sm">
                      <div className="flex items-center space-x-2">
                        <button
                          onClick={() => updateQuantity(item.id, item.quantity - 1)}
                          className="text-gray-500 hover:text-gray-700"
                        >
                          -
                        </button>
                        <span className="text-gray-500">{item.quantity}</span>
                        <button
                          onClick={() => updateQuantity(item.id, item.quantity + 1)}
                          className="text-gray-500 hover:text-gray-700"
                        >
                          +
                        </button>
                      </div>
                      <button
                        onClick={() => removeFromCart(item.id)}
                        className="font-medium text-blue-600 hover:text-blue-500"
                      >
                        移除
                      </button>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>

        {/* 购物车底部 */}
        <div className="border-t border-gray-200 py-6 px-4">
          <div className="flex justify-between text-base font-medium text-gray-900">
            <p>总计</p>
            <p>¥{total.toFixed(2)}</p>
          </div>
          <p className="mt-0.5 text-sm text-gray-500">
            运费和税费将在结算时计算
          </p>
          <div className="mt-6">
            <a
              href="/checkout"
              className="flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-blue-700"
            >
              结算
            </a>
          </div>
        </div>
      </div>
    </div>
  );
};

结算流程

结算表单

// components/CheckoutForm.tsx
import { useState } from 'react';

interface CheckoutFormProps {
  onSubmit: (data: CheckoutData) => void;
}

interface CheckoutData {
  name: string;
  email: string;
  address: string;
  phone: string;
  paymentMethod: 'credit-card' | 'alipay' | 'wechat';
}

const CheckoutForm: React.FC<CheckoutFormProps> = ({ onSubmit }) => {
  const [formData, setFormData] = useState<CheckoutData>({
    name: '',
    email: '',
    address: '',
    phone: '',
    paymentMethod: 'credit-card'
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit(formData);
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {/* 个人信息 */}
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-lg font-medium text-gray-900 mb-4">个人信息</h2>
        <div className="grid grid-cols-1 gap-6">
          <div>
            <label htmlFor="name" className="block text-sm font-medium text-gray-700">
              姓名
            </label>
            <input
              type="text"
              id="name"
              value={formData.name}
              onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
            />
          </div>

          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              邮箱
            </label>
            <input
              type="email"
              id="email"
              value={formData.email}
              onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
            />
          </div>

          <div>
            <label htmlFor="phone" className="block text-sm font-medium text-gray-700">
              电话
            </label>
            <input
              type="tel"
              id="phone"
              value={formData.phone}
              onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
            />
          </div>

          <div>
            <label htmlFor="address" className="block text-sm font-medium text-gray-700">
              地址
            </label>
            <textarea
              id="address"
              rows={3}
              value={formData.address}
              onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
            />
          </div>
        </div>
      </div>

      {/* 支付方式 */}
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-lg font-medium text-gray-900 mb-4">支付方式</h2>
        <div className="space-y-4">
          <div className="flex items-center">
            <input
              type="radio"
              id="credit-card"
              name="payment-method"
              value="credit-card"
              checked={formData.paymentMethod === 'credit-card'}
              onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
              className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
            />
            <label htmlFor="credit-card" className="ml-3 block text-sm font-medium text-gray-700">
              信用卡
            </label>
          </div>

          <div className="flex items-center">
            <input
              type="radio"
              id="alipay"
              name="payment-method"
              value="alipay"
              checked={formData.paymentMethod === 'alipay'}
              onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
              className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
            />
            <label htmlFor="alipay" className="ml-3 block text-sm font-medium text-gray-700">
              支付宝
            </label>
          </div>

          <div className="flex items-center">
            <input
              type="radio"
              id="wechat"
              name="payment-method"
              value="wechat"
              checked={formData.paymentMethod === 'wechat'}
              onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
              className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
            />
            <label htmlFor="wechat" className="ml-3 block text-sm font-medium text-gray-700">
              微信支付
            </label>
          </div>
        </div>
      </div>

      {/* 提交按钮 */}
      <div className="flex justify-end">
        <button
          type="submit"
          className="bg-blue-600 text-white px-6 py-3 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
        >
          提交订单
        </button>
      </div>
    </form>
  );
};

// 订单确认页面
const OrderConfirmation = () => {
  const { items, total } = useCart();

  return (
    <div className="max-w-3xl mx-auto px-4 py-8">
      <div className="bg-white rounded-lg shadow overflow-hidden">
        <div className="px-6 py-4 border-b border-gray-200">
          <h2 className="text-xl font-medium text-gray-900">订单确认</h2>
        </div>
        
        <div className="px-6 py-4">
          <div className="flow-root">
            <ul className="divide-y divide-gray-200">
              {items.map(item => (
                <li key={item.id} className="py-4">
                  <div className="flex items-center space-x-4">
                    <div className="flex-shrink-0">
                      <img 
                        src={item.image}
                        alt={item.title}
                        className="h-16 w-16 rounded-md object-cover"
                      />
                    </div>
                    <div className="flex-1 min-w-0">
                      <p className="text-sm font-medium text-gray-900 truncate">
                        {item.title}
                      </p>
                      <p className="text-sm text-gray-500">
                        数量: {item.quantity}
                      </p>
                    </div>
                    <div className="flex-shrink-0">
                      <p className="text-sm font-medium text-gray-900">
                        ¥{(item.price * item.quantity).toFixed(2)}
                      </p>
                    </div>
                  </div>
                </li>
              ))}
            </ul>
          </div>
          
          <div className="mt-6 border-t border-gray-200 pt-6">
            <div className="flex justify-between text-base font-medium text-gray-900">
              <p>总计</p>
              <p>¥{total.toFixed(2)}</p>
            </div>
            <p className="mt-2 text-sm text-gray-500">
              运费已包含
            </p>
          </div>
        </div>
      </div>
    </div>
  );
};

最佳实践

  1. 页面组织

    • 合理的组件拆分
    • 状态管理清晰
    • 复用公共组件
  2. 用户体验

    • 加载状态处理
    • 错误提示友好
    • 表单验证完善
  3. 性能优化

    • 图片懒加载
    • 组件按需加载
    • 状态更新优化
  4. 响应式设计

    • 移动端适配
    • 合理的布局
    • 交互体验优化
  5. 功能完善

    • 商品搜索
    • 订单管理
    • 用户中心

网站公告

今日签到

点亮在社区的每一天
去签到