394 lines
14 KiB
TypeScript
394 lines
14 KiB
TypeScript
'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>
|
||
)
|
||
}
|