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

商品列表

商品卡片组件

  1. // components/ProductCard.tsx
  2. interface ProductCardProps {
  3. product: {
  4. id: string;
  5. title: string;
  6. price: number;
  7. image: string;
  8. discount?: number;
  9. tags?: string[];
  10. };
  11. onAddToCart: (productId: string) => void;
  12. }
  13. const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => {
  14. return (
  15. <div className="group relative">
  16. {/* 商品图片 */}
  17. <div className="aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-200">
  18. <img
  19. src={product.image}
  20. alt={product.title}
  21. className="h-full w-full object-cover object-center group-hover:opacity-75 transition-opacity"
  22. />
  23. {product.discount && (
  24. <div className="absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded-md text-sm font-medium">
  25. -{product.discount}%
  26. </div>
  27. )}
  28. </div>
  29. {/* 商品信息 */}
  30. <div className="mt-4 flex justify-between">
  31. <div>
  32. <h3 className="text-sm text-gray-700">
  33. <a href={`/product/${product.id}`}>
  34. <span aria-hidden="true" className="absolute inset-0" />
  35. {product.title}
  36. </a>
  37. </h3>
  38. <div className="mt-1 flex items-center space-x-2">
  39. <p className="text-lg font-medium text-gray-900">
  40. ¥{product.price}
  41. </p>
  42. {product.discount && (
  43. <p className="text-sm text-gray-500 line-through">
  44. ¥{(product.price * (100 + product.discount) / 100).toFixed(2)}
  45. </p>
  46. )}
  47. </div>
  48. </div>
  49. <button
  50. onClick={() => onAddToCart(product.id)}
  51. 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"
  52. >
  53. <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  54. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
  55. </svg>
  56. </button>
  57. </div>
  58. {/* 商品标签 */}
  59. {product.tags && product.tags.length > 0 && (
  60. <div className="mt-2 flex flex-wrap gap-1">
  61. {product.tags.map(tag => (
  62. <span
  63. key={tag}
  64. className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800"
  65. >
  66. {tag}
  67. </span>
  68. ))}
  69. </div>
  70. )}
  71. </div>
  72. );
  73. };

商品列表页面

  1. // pages/ProductList.tsx
  2. import { useState } from 'react';
  3. import ProductCard from '../components/ProductCard';
  4. import { useCart } from '../hooks/useCart';
  5. const filters = [
  6. { id: 'category', name: '分类', options: ['全部', '电子产品', '服装', '食品'] },
  7. { id: 'price', name: '价格', options: ['全部', '0-100', '100-500', '500+'] },
  8. // ... 更多筛选选项
  9. ];
  10. const ProductList = () => {
  11. const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
  12. const { addToCart } = useCart();
  13. return (
  14. <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
  15. {/* 筛选器 */}
  16. <div className="py-4 border-b border-gray-200">
  17. <div className="flex flex-wrap gap-4">
  18. {filters.map(filter => (
  19. <div key={filter.id} className="relative">
  20. <select
  21. 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"
  22. value={activeFilters[filter.id] || ''}
  23. onChange={(e) => {
  24. setActiveFilters(prev => ({
  25. ...prev,
  26. [filter.id]: e.target.value
  27. }));
  28. }}
  29. >
  30. <option value="">{filter.name}</option>
  31. {filter.options.map(option => (
  32. <option key={option} value={option}>
  33. {option}
  34. </option>
  35. ))}
  36. </select>
  37. <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
  38. <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
  39. <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" />
  40. </svg>
  41. </div>
  42. </div>
  43. ))}
  44. </div>
  45. </div>
  46. {/* 商品网格 */}
  47. <div className="mt-6 grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-4">
  48. {products.map(product => (
  49. <ProductCard
  50. key={product.id}
  51. product={product}
  52. onAddToCart={addToCart}
  53. />
  54. ))}
  55. </div>
  56. {/* 分页 */}
  57. <div className="mt-8 flex justify-center">
  58. <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
  59. <a
  60. href="#"
  61. 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"
  62. >
  63. 上一页
  64. </a>
  65. {/* 页码 */}
  66. <a
  67. href="#"
  68. 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"
  69. >
  70. 1
  71. </a>
  72. <a
  73. href="#"
  74. 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"
  75. >
  76. 下一页
  77. </a>
  78. </nav>
  79. </div>
  80. </div>
  81. );
  82. };

购物车功能

购物车 Hook

  1. // hooks/useCart.ts
  2. import { useState, useCallback } from 'react';
  3. interface CartItem {
  4. id: string;
  5. quantity: number;
  6. price: number;
  7. title: string;
  8. image: string;
  9. }
  10. export const useCart = () => {
  11. const [items, setItems] = useState<CartItem[]>([]);
  12. const addToCart = useCallback((product: Omit<CartItem, 'quantity'>) => {
  13. setItems(prev => {
  14. const existingItem = prev.find(item => item.id === product.id);
  15. if (existingItem) {
  16. return prev.map(item =>
  17. item.id === product.id
  18. ? { ...item, quantity: item.quantity + 1 }
  19. : item
  20. );
  21. }
  22. return [...prev, { ...product, quantity: 1 }];
  23. });
  24. }, []);
  25. const removeFromCart = useCallback((productId: string) => {
  26. setItems(prev => prev.filter(item => item.id !== productId));
  27. }, []);
  28. const updateQuantity = useCallback((productId: string, quantity: number) => {
  29. setItems(prev =>
  30. prev.map(item =>
  31. item.id === productId
  32. ? { ...item, quantity: Math.max(0, quantity) }
  33. : item
  34. ).filter(item => item.quantity > 0)
  35. );
  36. }, []);
  37. const total = items.reduce(
  38. (sum, item) => sum + item.price * item.quantity,
  39. 0
  40. );
  41. return {
  42. items,
  43. addToCart,
  44. removeFromCart,
  45. updateQuantity,
  46. total
  47. };
  48. };

购物车组件

  1. // components/Cart.tsx
  2. import { useCart } from '../hooks/useCart';
  3. const Cart = () => {
  4. const { items, removeFromCart, updateQuantity, total } = useCart();
  5. return (
  6. <div className="fixed inset-y-0 right-0 w-96 bg-white shadow-xl">
  7. <div className="flex flex-col h-full">
  8. {/* 购物车头部 */}
  9. <div className="px-4 py-6 bg-gray-50">
  10. <h2 className="text-lg font-medium text-gray-900">购物车</h2>
  11. </div>
  12. {/* 购物车列表 */}
  13. <div className="flex-1 overflow-y-auto py-6 px-4">
  14. {items.length === 0 ? (
  15. <div className="text-center py-12">
  16. <svg
  17. className="mx-auto h-12 w-12 text-gray-400"
  18. fill="none"
  19. viewBox="0 0 24 24"
  20. stroke="currentColor"
  21. >
  22. <path
  23. strokeLinecap="round"
  24. strokeLinejoin="round"
  25. strokeWidth={2}
  26. d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
  27. />
  28. </svg>
  29. <p className="mt-4 text-sm text-gray-500">
  30. 购物车是空的
  31. </p>
  32. </div>
  33. ) : (
  34. <div className="space-y-6">
  35. {items.map(item => (
  36. <div key={item.id} className="flex">
  37. <img
  38. src={item.image}
  39. alt={item.title}
  40. className="h-20 w-20 flex-shrink-0 rounded-md object-cover"
  41. />
  42. <div className="ml-4 flex flex-1 flex-col">
  43. <div>
  44. <div className="flex justify-between text-base font-medium text-gray-900">
  45. <h3>{item.title}</h3>
  46. <p className="ml-4">¥{item.price}</p>
  47. </div>
  48. </div>
  49. <div className="flex flex-1 items-end justify-between text-sm">
  50. <div className="flex items-center space-x-2">
  51. <button
  52. onClick={() => updateQuantity(item.id, item.quantity - 1)}
  53. className="text-gray-500 hover:text-gray-700"
  54. >
  55. -
  56. </button>
  57. <span className="text-gray-500">{item.quantity}</span>
  58. <button
  59. onClick={() => updateQuantity(item.id, item.quantity + 1)}
  60. className="text-gray-500 hover:text-gray-700"
  61. >
  62. +
  63. </button>
  64. </div>
  65. <button
  66. onClick={() => removeFromCart(item.id)}
  67. className="font-medium text-blue-600 hover:text-blue-500"
  68. >
  69. 移除
  70. </button>
  71. </div>
  72. </div>
  73. </div>
  74. ))}
  75. </div>
  76. )}
  77. </div>
  78. {/* 购物车底部 */}
  79. <div className="border-t border-gray-200 py-6 px-4">
  80. <div className="flex justify-between text-base font-medium text-gray-900">
  81. <p>总计</p>
  82. <p>¥{total.toFixed(2)}</p>
  83. </div>
  84. <p className="mt-0.5 text-sm text-gray-500">
  85. 运费和税费将在结算时计算
  86. </p>
  87. <div className="mt-6">
  88. <a
  89. href="/checkout"
  90. 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"
  91. >
  92. 结算
  93. </a>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. );
  99. };

结算流程

结算表单

  1. // components/CheckoutForm.tsx
  2. import { useState } from 'react';
  3. interface CheckoutFormProps {
  4. onSubmit: (data: CheckoutData) => void;
  5. }
  6. interface CheckoutData {
  7. name: string;
  8. email: string;
  9. address: string;
  10. phone: string;
  11. paymentMethod: 'credit-card' | 'alipay' | 'wechat';
  12. }
  13. const CheckoutForm: React.FC<CheckoutFormProps> = ({ onSubmit }) => {
  14. const [formData, setFormData] = useState<CheckoutData>({
  15. name: '',
  16. email: '',
  17. address: '',
  18. phone: '',
  19. paymentMethod: 'credit-card'
  20. });
  21. const handleSubmit = (e: React.FormEvent) => {
  22. e.preventDefault();
  23. onSubmit(formData);
  24. };
  25. return (
  26. <form onSubmit={handleSubmit} className="space-y-6">
  27. {/* 个人信息 */}
  28. <div className="bg-white p-6 rounded-lg shadow">
  29. <h2 className="text-lg font-medium text-gray-900 mb-4">个人信息</h2>
  30. <div className="grid grid-cols-1 gap-6">
  31. <div>
  32. <label htmlFor="name" className="block text-sm font-medium text-gray-700">
  33. 姓名
  34. </label>
  35. <input
  36. type="text"
  37. id="name"
  38. value={formData.name}
  39. onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
  40. className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
  41. />
  42. </div>
  43. <div>
  44. <label htmlFor="email" className="block text-sm font-medium text-gray-700">
  45. 邮箱
  46. </label>
  47. <input
  48. type="email"
  49. id="email"
  50. value={formData.email}
  51. onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
  52. className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
  53. />
  54. </div>
  55. <div>
  56. <label htmlFor="phone" className="block text-sm font-medium text-gray-700">
  57. 电话
  58. </label>
  59. <input
  60. type="tel"
  61. id="phone"
  62. value={formData.phone}
  63. onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
  64. className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
  65. />
  66. </div>
  67. <div>
  68. <label htmlFor="address" className="block text-sm font-medium text-gray-700">
  69. 地址
  70. </label>
  71. <textarea
  72. id="address"
  73. rows={3}
  74. value={formData.address}
  75. onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
  76. className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
  77. />
  78. </div>
  79. </div>
  80. </div>
  81. {/* 支付方式 */}
  82. <div className="bg-white p-6 rounded-lg shadow">
  83. <h2 className="text-lg font-medium text-gray-900 mb-4">支付方式</h2>
  84. <div className="space-y-4">
  85. <div className="flex items-center">
  86. <input
  87. type="radio"
  88. id="credit-card"
  89. name="payment-method"
  90. value="credit-card"
  91. checked={formData.paymentMethod === 'credit-card'}
  92. onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
  93. className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
  94. />
  95. <label htmlFor="credit-card" className="ml-3 block text-sm font-medium text-gray-700">
  96. 信用卡
  97. </label>
  98. </div>
  99. <div className="flex items-center">
  100. <input
  101. type="radio"
  102. id="alipay"
  103. name="payment-method"
  104. value="alipay"
  105. checked={formData.paymentMethod === 'alipay'}
  106. onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
  107. className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
  108. />
  109. <label htmlFor="alipay" className="ml-3 block text-sm font-medium text-gray-700">
  110. 支付宝
  111. </label>
  112. </div>
  113. <div className="flex items-center">
  114. <input
  115. type="radio"
  116. id="wechat"
  117. name="payment-method"
  118. value="wechat"
  119. checked={formData.paymentMethod === 'wechat'}
  120. onChange={(e) => setFormData(prev => ({ ...prev, paymentMethod: e.target.value as CheckoutData['paymentMethod'] }))}
  121. className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
  122. />
  123. <label htmlFor="wechat" className="ml-3 block text-sm font-medium text-gray-700">
  124. 微信支付
  125. </label>
  126. </div>
  127. </div>
  128. </div>
  129. {/* 提交按钮 */}
  130. <div className="flex justify-end">
  131. <button
  132. type="submit"
  133. 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"
  134. >
  135. 提交订单
  136. </button>
  137. </div>
  138. </form>
  139. );
  140. };
  141. // 订单确认页面
  142. const OrderConfirmation = () => {
  143. const { items, total } = useCart();
  144. return (
  145. <div className="max-w-3xl mx-auto px-4 py-8">
  146. <div className="bg-white rounded-lg shadow overflow-hidden">
  147. <div className="px-6 py-4 border-b border-gray-200">
  148. <h2 className="text-xl font-medium text-gray-900">订单确认</h2>
  149. </div>
  150. <div className="px-6 py-4">
  151. <div className="flow-root">
  152. <ul className="divide-y divide-gray-200">
  153. {items.map(item => (
  154. <li key={item.id} className="py-4">
  155. <div className="flex items-center space-x-4">
  156. <div className="flex-shrink-0">
  157. <img
  158. src={item.image}
  159. alt={item.title}
  160. className="h-16 w-16 rounded-md object-cover"
  161. />
  162. </div>
  163. <div className="flex-1 min-w-0">
  164. <p className="text-sm font-medium text-gray-900 truncate">
  165. {item.title}
  166. </p>
  167. <p className="text-sm text-gray-500">
  168. 数量: {item.quantity}
  169. </p>
  170. </div>
  171. <div className="flex-shrink-0">
  172. <p className="text-sm font-medium text-gray-900">
  173. ¥{(item.price * item.quantity).toFixed(2)}
  174. </p>
  175. </div>
  176. </div>
  177. </li>
  178. ))}
  179. </ul>
  180. </div>
  181. <div className="mt-6 border-t border-gray-200 pt-6">
  182. <div className="flex justify-between text-base font-medium text-gray-900">
  183. <p>总计</p>
  184. <p>¥{total.toFixed(2)}</p>
  185. </div>
  186. <p className="mt-2 text-sm text-gray-500">
  187. 运费已包含
  188. </p>
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. );
  194. };

最佳实践

  1. 页面组织

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

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

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

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

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