本节将介绍如何使用 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"><imgsrc={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><buttononClick={() => 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 => (<spankey={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"><selectclassName="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 => (<ProductCardkey={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"><ahref="#"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>{/* 页码 */}<ahref="#"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><ahref="#"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"><svgclassName="mx-auto h-12 w-12 text-gray-400"fill="none"viewBox="0 0 24 24"stroke="currentColor"><pathstrokeLinecap="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"><imgsrc={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"><buttononClick={() => updateQuantity(item.id, item.quantity - 1)}className="text-gray-500 hover:text-gray-700">-</button><span className="text-gray-500">{item.quantity}</span><buttononClick={() => updateQuantity(item.id, item.quantity + 1)}className="text-gray-500 hover:text-gray-700">+</button></div><buttononClick={() => 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"><ahref="/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><inputtype="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><inputtype="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><inputtype="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><textareaid="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"><inputtype="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"><inputtype="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"><inputtype="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"><buttontype="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>);
};
最佳实践
-
页面组织
- 合理的组件拆分
- 状态管理清晰
- 复用公共组件
-
用户体验
- 加载状态处理
- 错误提示友好
- 表单验证完善
-
性能优化
- 图片懒加载
- 组件按需加载
- 状态更新优化
-
响应式设计
- 移动端适配
- 合理的布局
- 交互体验优化
-
功能完善
- 商品搜索
- 订单管理
- 用户中心