本节将介绍如何使用 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>
);
};
最佳实践
页面组织
- 合理的组件拆分
- 状态管理清晰
- 复用公共组件
用户体验
- 加载状态处理
- 错误提示友好
- 表单验证完善
性能优化
- 图片懒加载
- 组件按需加载
- 状态更新优化
响应式设计
- 移动端适配
- 合理的布局
- 交互体验优化
功能完善
- 商品搜索
- 订单管理
- 用户中心