2025-06-22 11:34:32 +08:00

394 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}