This commit is contained in:
2025-06-22 11:34:32 +08:00
parent 142992a6c9
commit 7d139b22db
55 changed files with 11035 additions and 105 deletions
+393
View File
@@ -0,0 +1,393 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft } from 'lucide-react'
interface CartItem {
id: string
quantity: number
component: {
id: string
name: string
brand: string
price: number
imageUrl?: string | null
stock: number
componentType: {
name: string
}
}
}
export default function CartPage() {
const [cartItems, setCartItems] = useState<CartItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isUpdating, setIsUpdating] = useState<string | null>(null)
useEffect(() => {
loadCartItems()
}, [])
const loadCartItems = async () => {
try {
const token = localStorage.getItem('token')
if (!token) {
setIsLoading(false)
return
}
const response = await fetch('/api/cart', {
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
const items = await response.json()
setCartItems(items)
} else if (response.status === 401) {
// 登录过期,清除token
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
}
} catch (error) {
console.error('加载购物车失败:', error)
} finally {
setIsLoading(false)
}
}
const updateQuantity = async (cartItemId: string, newQuantity: number) => {
if (newQuantity < 1) return
setIsUpdating(cartItemId)
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/cart/${cartItemId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ quantity: newQuantity })
})
if (response.ok) {
// 重新加载购物车
await loadCartItems()
} else {
const data = await response.json()
alert(data.message || '更新失败')
}
} catch (error) {
console.error('更新数量失败:', error)
alert('更新失败')
} finally {
setIsUpdating(null)
}
}
const removeFromCart = async (cartItemId: string) => {
if (!confirm('确定要从购物车中移除这个商品吗?')) return
setIsUpdating(cartItemId)
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/cart/${cartItemId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
// 重新加载购物车
await loadCartItems()
} else {
const data = await response.json()
alert(data.message || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
alert('删除失败')
} finally {
setIsUpdating(null)
}
}
const clearCart = async () => {
if (!confirm('确定要清空购物车吗?')) return
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/cart', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
setCartItems([])
} else {
const data = await response.json()
alert(data.message || '清空失败')
}
} catch (error) {
console.error('清空购物车失败:', error)
alert('清空失败')
}
}
const getTotalPrice = () => {
return cartItems.reduce((total, item) => {
return total + (item.component.price * item.quantity)
}, 0)
}
const getTotalItems = () => {
return cartItems.reduce((total, item) => total + item.quantity, 0)
}
const createOrder = async () => {
if (cartItems.length === 0) {
alert('购物车为空')
return
}
try {
const user = JSON.parse(localStorage.getItem('user') || 'null')
if (!user) {
alert('请先登录')
window.location.href = '/login'
return
}
const orderItems = cartItems.map(item => ({
componentId: item.component.id,
quantity: item.quantity,
price: item.component.price
}))
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
items: orderItems,
totalAmount: getTotalPrice()
})
})
if (response.ok) {
// 清空购物车
await clearCart()
alert('订单创建成功!')
window.location.href = '/orders'
} else {
const data = await response.json()
alert(data.message || '订单创建失败')
}
} catch (error) {
console.error('创建订单失败:', error)
alert('订单创建失败,请重试')
}
}
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">加载中...</p>
</div>
</div>
)
}
const user = JSON.parse(localStorage.getItem('user') || 'null')
if (!user) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<ShoppingBag className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-gray-900 mb-4">请先登录</h2>
<p className="text-gray-600 mb-8">登录后即可查看您的购物车</p>
<Link
href="/login"
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
>
立即登录
</Link>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-4">
<Link
href="/components"
className="flex items-center text-blue-600 hover:text-blue-800"
>
<ArrowLeft className="h-5 w-5 mr-2" />
继续购物
</Link>
<h1 className="text-3xl font-bold text-gray-900">购物车</h1>
</div>
{cartItems.length > 0 && (
<button
onClick={clearCart}
className="text-red-600 hover:text-red-800 text-sm font-medium"
>
清空购物车
</button>
)}
</div>
{cartItems.length === 0 ? (
<div className="text-center py-16">
<ShoppingBag className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-gray-900 mb-4">购物车为空</h2>
<p className="text-gray-600 mb-8">还没有添加任何商品到购物车</p>
<Link
href="/components"
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
>
开始购物
</Link>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm">
<div className="p-6 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">
商品列表 ({getTotalItems()} 件商品)
</h2>
</div>
<div className="divide-y divide-gray-200">
{cartItems.map((item) => (
<div key={item.id} className="p-6">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0 w-20 h-20 bg-gray-100 rounded-lg overflow-hidden">
{item.component.imageUrl ? (
<img
src={item.component.imageUrl}
alt={item.component.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<ShoppingBag className="h-8 w-8 text-gray-400" />
</div>
)}
</div>
<div className="flex-1">
<div className="flex justify-between">
<div>
<h3 className="font-medium text-gray-900">{item.component.name}</h3>
<p className="text-sm text-gray-600">{item.component.brand}</p>
<p className="text-xs text-gray-500">{item.component.componentType.name}</p>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">¥{item.component.price}</p>
<p className="text-sm text-gray-600">库存: {item.component.stock}</p>
</div>
</div>
<div className="flex items-center justify-between mt-4">
s <div className="flex items-center space-x-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1 || isUpdating === item.id}
className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Minus className="h-4 w-4" />
</button>
<span className="w-12 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
disabled={item.quantity >= item.component.stock || isUpdating === item.id}
className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="h-4 w-4" />
</button>
</div>
<div className="flex items-center space-x-4">
<p className="font-semibold text-lg">
¥{(item.component.price * item.quantity).toFixed(2)}
</p>
<button
onClick={() => removeFromCart(item.id)}
disabled={isUpdating === item.id}
className="text-red-600 hover:text-red-800 disabled:opacity-50"
>
<Trash2 className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Order Summary */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-sm p-6 sticky top-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">订单摘要</h2>
<div className="space-y-3 mb-6">
<div className="flex justify-between">
<span className="text-gray-600">商品总数:</span>
<span className="font-medium">{getTotalItems()} 件</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">商品总价:</span>
<span className="font-medium">¥{getTotalPrice().toFixed(2)}</span>
</div>
<div className="border-t border-gray-200 pt-3">
<div className="flex justify-between">
<span className="font-semibold text-lg">总计:</span>
<span className="font-semibold text-lg text-red-600">
¥{getTotalPrice().toFixed(2)}
</span>
</div>
</div>
</div>
<button
onClick={createOrder}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
结算订单
</button>
<p className="text-xs text-gray-500 mt-4 text-center">
点击结算将创建订单并清空购物车
</p>
</div>
</div>
</div>
)}
</div>
</div>
)
}