258 lines
7.4 KiB
JavaScript
258 lines
7.4 KiB
JavaScript
"use client";
|
||
|
||
import React, { useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
|
||
export default function Cart() {
|
||
const router = useRouter();
|
||
const [cartItems, setCartItems] = React.useState([]);
|
||
const [loading, setLoading] = React.useState(true);
|
||
const [error, setError] = React.useState(null);
|
||
const [recommendProducts, setRecommendProducts] = React.useState([]);
|
||
|
||
useEffect(() => {
|
||
const fetchCartItems = async () => {
|
||
try {
|
||
const token = localStorage.getItem("token");
|
||
if (!token) {
|
||
setCartItems([]);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
const res = await fetch("/api/cart", {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error("获取购物车失败");
|
||
}
|
||
const data = await res.json();
|
||
setCartItems(data);
|
||
} catch (err) {
|
||
setError(err.message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchCartItems();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const fetchRecommendProducts = async () => {
|
||
try {
|
||
const res = await fetch("/api/products");
|
||
if (!res.ok) {
|
||
throw new Error("获取推荐商品失败");
|
||
}
|
||
const data = await res.json();
|
||
// 随机获取4个商品作为推荐
|
||
const shuffled = data.sort(() => 0.5 - Math.random());
|
||
setRecommendProducts(shuffled.slice(0, 4));
|
||
} catch (err) {
|
||
console.error("获取推荐商品失败:", err);
|
||
}
|
||
};
|
||
|
||
fetchRecommendProducts();
|
||
}, []);
|
||
|
||
const calculateTotal = () => {
|
||
return cartItems.reduce(
|
||
(total, item) => total + item.productId.price * item.quantity,
|
||
0
|
||
);
|
||
};
|
||
|
||
const handleCheckout = () => {
|
||
const productParams = cartItems
|
||
.map((item) => `${item.productId._id}:${item.quantity}`)
|
||
.join(",");
|
||
router.push(`/checkout?items=${productParams}`);
|
||
};
|
||
|
||
const handleDelete = async (itemId) => {
|
||
try {
|
||
const token = localStorage.getItem("token");
|
||
if (!token) {
|
||
router.push("/login");
|
||
return;
|
||
}
|
||
|
||
const res = await fetch("/api/cart", {
|
||
method: "DELETE",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify({ cartItemId: itemId }),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error("删除失败");
|
||
}
|
||
|
||
// 更新本地状态
|
||
setCartItems(cartItems.filter((item) => item._id !== itemId));
|
||
} catch (error) {
|
||
console.error("删除失败:", error);
|
||
}
|
||
};
|
||
|
||
const handleUpdateQuantity = async (itemId, newQuantity) => {
|
||
try {
|
||
const token = localStorage.getItem("token");
|
||
if (!token) {
|
||
router.push("/login");
|
||
return;
|
||
}
|
||
|
||
const res = await fetch("/api/cart", {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify({
|
||
cartItemId: itemId,
|
||
quantity: newQuantity,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error("更新数量失败");
|
||
}
|
||
|
||
// 更新本地状态
|
||
setCartItems(
|
||
cartItems.map((item) =>
|
||
item._id === itemId ? { ...item, quantity: newQuantity } : item
|
||
)
|
||
);
|
||
} catch (error) {
|
||
console.error("更新失败:", error);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="p-4">加载中...</div>;
|
||
}
|
||
|
||
if (error) {
|
||
return <div className="p-4 text-red-500">错误: {error}</div>;
|
||
}
|
||
|
||
return (
|
||
<div className="p-4">
|
||
<h1 className="text-2xl font-bold mb-4">购物车</h1>
|
||
|
||
{cartItems.length === 0 ? (
|
||
<div className="text-center py-8">
|
||
<p>购物车为空</p>
|
||
<p className="text-gray-500">赶紧去逛逛,购买心仪的商品吧</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="space-y-4">
|
||
{cartItems.map((item) => (
|
||
<div key={item._id} className="flex items-center border-b pb-4">
|
||
<img
|
||
src={item.productId.imageUrl}
|
||
alt={item.productId.title}
|
||
className="w-24 h-24 object-cover rounded"
|
||
/>
|
||
<div className="ml-4 flex-1">
|
||
<h3 className="font-medium">{item.productId.title}</h3>
|
||
<p className="text-red-500">
|
||
¥{item.productId.price.toFixed(2)}
|
||
</p>
|
||
<div className="flex items-center justify-between mt-2">
|
||
<div className="flex items-center">
|
||
<button
|
||
className="px-2 py-1 border"
|
||
onClick={() =>
|
||
handleUpdateQuantity(
|
||
item._id,
|
||
Math.max(1, item.quantity - 1)
|
||
)
|
||
}
|
||
>
|
||
-
|
||
</button>
|
||
<span className="px-4">{item.quantity}</span>
|
||
<button
|
||
className="px-2 py-1 border"
|
||
onClick={() =>
|
||
handleUpdateQuantity(item._id, item.quantity + 1)
|
||
}
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
<button
|
||
onClick={() => handleDelete(item._id)}
|
||
className="text-red-500 text-sm"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-6 flex justify-between items-center">
|
||
<span className="text-lg">总计:</span>
|
||
<span className="text-xl text-red-500 font-bold">
|
||
¥{calculateTotal().toFixed(2)}
|
||
</span>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleCheckout}
|
||
className="w-full bg-red-500 text-white py-3 rounded-lg mt-4"
|
||
>
|
||
结算
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{/* 推荐商品板块 - 移到条件渲染外部 */}
|
||
<div className="mt-8">
|
||
<h2 className="text-xl font-bold mb-4">猜你喜欢</h2>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{recommendProducts.map((product) => (
|
||
<div
|
||
key={product._id}
|
||
className="bg-white rounded-lg overflow-hidden shadow-sm cursor-pointer"
|
||
onClick={() => router.push(`/good/${product._id}`)}
|
||
>
|
||
<img
|
||
src={product.imageUrl}
|
||
alt={product.title}
|
||
className="w-full aspect-square object-cover"
|
||
/>
|
||
<div className="p-2">
|
||
<h3 className="text-sm font-medium line-clamp-2">
|
||
{product.title}
|
||
</h3>
|
||
<div className="mt-2 flex items-center justify-between">
|
||
<span className="text-red-500">¥{product.price}</span>
|
||
{product.isVip && (
|
||
<span className="text-xs px-1 text-yellow-600 border border-yellow-600 rounded">
|
||
VIP
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|