v1
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user