2025-02-10 19:24:19 +08:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, Suspense } from "react";
|
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
|
|
|
import Image from "next/image";
|
2025-02-11 16:50:04 +08:00
|
|
|
|
import PaymentModal from "@/components/PaymentModal";
|
2025-02-10 19:24:19 +08:00
|
|
|
|
|
|
|
|
|
function CheckoutContent() {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const [products, setProducts] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
|
const [address, setAddress] = useState(null);
|
2025-02-11 16:50:04 +08:00
|
|
|
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
2025-02-10 19:24:19 +08:00
|
|
|
|
|
|
|
|
|
// 获取地址的函数
|
|
|
|
|
const fetchAddress = async (token) => {
|
|
|
|
|
const addressRes = await fetch("/api/address", {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (addressRes.ok) {
|
|
|
|
|
const addresses = await addressRes.json();
|
|
|
|
|
const selectedAddress = localStorage.getItem("selectedAddress");
|
|
|
|
|
|
|
|
|
|
if (selectedAddress) {
|
|
|
|
|
const parsedSelectedAddress = JSON.parse(selectedAddress);
|
|
|
|
|
// 在服务器返回的地址列表中查找选中的地址
|
|
|
|
|
const matchedAddress = addresses.find(
|
|
|
|
|
(addr) => addr._id === parsedSelectedAddress._id
|
|
|
|
|
);
|
|
|
|
|
if (matchedAddress) {
|
|
|
|
|
setAddress(matchedAddress);
|
|
|
|
|
localStorage.removeItem("selectedAddress");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果没有匹配到选中的地址,使用默认地址
|
|
|
|
|
const defaultAddress = addresses.find((addr) => addr.isDefault);
|
|
|
|
|
if (defaultAddress) {
|
|
|
|
|
setAddress(defaultAddress);
|
|
|
|
|
} else if (addresses.length > 0) {
|
|
|
|
|
setAddress(addresses[0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取商品信息的函数
|
|
|
|
|
const fetchProducts = async () => {
|
|
|
|
|
const items = searchParams.get("items");
|
|
|
|
|
if (items) {
|
|
|
|
|
const productPromises = items.split(",").map(async (item) => {
|
|
|
|
|
const [productId, quantity] = item.split(":");
|
|
|
|
|
const res = await fetch(`/api/products/${productId}`);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error("获取商品信息失败");
|
|
|
|
|
}
|
|
|
|
|
const product = await res.json();
|
|
|
|
|
return { ...product, quantity: parseInt(quantity) };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const productsData = await Promise.all(productPromises);
|
|
|
|
|
setProducts(productsData);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const productId = searchParams.get("products");
|
|
|
|
|
const quantity = searchParams.get("quantity");
|
|
|
|
|
|
|
|
|
|
if (!productId || !quantity) {
|
|
|
|
|
throw new Error("商品信息不完整");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await fetch(`/api/products/${productId}`);
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error("获取商品信息失败");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const product = await res.json();
|
|
|
|
|
setProducts([{ ...product, quantity: parseInt(quantity) }]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const token = localStorage.getItem("token");
|
|
|
|
|
if (!token) {
|
|
|
|
|
router.push("/login");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await fetchAddress(token);
|
|
|
|
|
await fetchProducts();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err.message);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
if (loading) return <div className="p-4">加载中...</div>;
|
|
|
|
|
if (error) return <div className="p-4 text-red-500">错误: {error}</div>;
|
|
|
|
|
|
|
|
|
|
const total = products.reduce(
|
|
|
|
|
(sum, item) => sum + item.price * item.quantity,
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getReturnUrl = () => {
|
|
|
|
|
const items = searchParams.get("items");
|
|
|
|
|
const products = searchParams.get("products");
|
|
|
|
|
const quantity = searchParams.get("quantity");
|
|
|
|
|
|
|
|
|
|
let params = new URLSearchParams();
|
|
|
|
|
if (items) {
|
|
|
|
|
params.append("items", items);
|
|
|
|
|
} else if (products && quantity) {
|
|
|
|
|
params.append("products", products);
|
|
|
|
|
params.append("quantity", quantity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `/address?returnUrl=${encodeURIComponent(
|
|
|
|
|
"/checkout"
|
|
|
|
|
)}&${params.toString()}`;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-11 16:50:04 +08:00
|
|
|
|
const handleSubmitOrder = () => {
|
|
|
|
|
setShowPaymentModal(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-10 19:24:19 +08:00
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-50 pb-20">
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<h1 className="text-xl font-bold mb-4">确认订单</h1>
|
|
|
|
|
|
|
|
|
|
{/* 收货地址 */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => router.replace(getReturnUrl())}
|
|
|
|
|
className="block w-full bg-white rounded-lg p-4 mb-4 text-left"
|
|
|
|
|
>
|
|
|
|
|
{address ? (
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="font-medium">{address.name}</span>
|
|
|
|
|
<span>{address.phone}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-gray-500 text-sm mt-1">
|
|
|
|
|
{address.address}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<svg
|
|
|
|
|
className="w-4 h-4 text-gray-400"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M9 5l7 7-7 7"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex justify-between items-center text-gray-500">
|
|
|
|
|
<span>请选择收货地址</span>
|
|
|
|
|
<svg
|
|
|
|
|
className="w-4 h-4"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
d="M9 5l7 7-7 7"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* 商品列表 */}
|
|
|
|
|
<div className="bg-white rounded-lg p-4 mb-4">
|
|
|
|
|
{products.map((product) => (
|
|
|
|
|
<div key={product._id} className="flex items-center">
|
|
|
|
|
<Image
|
|
|
|
|
src={product.imageUrl}
|
|
|
|
|
alt={product.title}
|
|
|
|
|
width={80}
|
|
|
|
|
height={80}
|
|
|
|
|
className="rounded"
|
|
|
|
|
/>
|
|
|
|
|
<div className="ml-4 flex-1">
|
|
|
|
|
<h3 className="font-medium">{product.title}</h3>
|
|
|
|
|
<div className="flex justify-between mt-2">
|
|
|
|
|
<span className="text-red-500">¥{product.price}</span>
|
|
|
|
|
<span>x{product.quantity}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 订单总计 */}
|
|
|
|
|
<div className="fixed bottom-0 left-0 right-0 bg-white p-4 border-t">
|
|
|
|
|
<div className="flex justify-between items-center mb-4">
|
|
|
|
|
<span>总计:</span>
|
|
|
|
|
<span className="text-xl text-red-500 font-bold">¥{total}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
className="w-full bg-red-500 text-white py-3 rounded-full"
|
|
|
|
|
disabled={!address}
|
2025-02-11 16:50:04 +08:00
|
|
|
|
onClick={handleSubmitOrder}
|
2025-02-10 19:24:19 +08:00
|
|
|
|
>
|
|
|
|
|
提交订单
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-02-11 16:50:04 +08:00
|
|
|
|
|
|
|
|
|
<PaymentModal
|
|
|
|
|
isOpen={showPaymentModal}
|
|
|
|
|
onClose={() => setShowPaymentModal(false)}
|
|
|
|
|
amount={total}
|
|
|
|
|
/>
|
2025-02-10 19:24:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function Checkout() {
|
|
|
|
|
return (
|
|
|
|
|
<Suspense fallback={<div className="p-4">加载中...</div>}>
|
|
|
|
|
<CheckoutContent />
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|