v1
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ShoppingCart, Plus, Minus } from 'lucide-react'
|
||||
|
||||
interface AddToCartButtonProps {
|
||||
componentId: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AddToCartButton({ componentId, disabled = false, className = '' }: AddToCartButtonProps) {
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const handleAddToCart = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
alert('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/cart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
componentId,
|
||||
quantity
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// 触发购物车更新事件
|
||||
window.dispatchEvent(new Event('cart-updated'))
|
||||
alert('已添加到购物车!')
|
||||
} else if (response.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
alert('登录已过期,请重新登录')
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
const data = await response.json()
|
||||
alert(data.message || '添加失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
alert('添加失败,请重试')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* Quantity Selector */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-600">数量:</span>
|
||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
disabled={quantity <= 1 || disabled}
|
||||
className="p-2 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="px-4 py-2 text-center min-w-[50px]">{quantity}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
disabled={disabled}
|
||||
className="p-2 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={disabled || isLoading}
|
||||
className={`
|
||||
flex items-center justify-center space-x-2 w-full py-3 px-6 rounded-lg font-medium transition-colors
|
||||
${disabled
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}
|
||||
${isLoading ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<span>
|
||||
{isLoading ? '添加中...' : disabled ? '缺货' : '加入购物车'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { ShoppingCart, Check } from 'lucide-react'
|
||||
|
||||
interface Component {
|
||||
id: string
|
||||
name: string
|
||||
brand: string
|
||||
model: string
|
||||
price: number
|
||||
description?: string | null
|
||||
imageUrl?: string | null
|
||||
stock: number
|
||||
componentType?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ComponentCardProps {
|
||||
component: Component
|
||||
}
|
||||
|
||||
export function ComponentCard({ component }: ComponentCardProps) {
|
||||
const [isAdding, setIsAdding] = useState(false)
|
||||
const [isAdded, setIsAdded] = useState(false)
|
||||
const addToCart = async (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (component.stock <= 0 || isAdding) return
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
alert('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
setIsAdding(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/cart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
componentId: component.id,
|
||||
quantity: 1
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
setIsAdded(true)
|
||||
setTimeout(() => setIsAdded(false), 2000)
|
||||
} else if (response.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
alert('登录已过期,请重新登录')
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
const data = await response.json()
|
||||
alert(data.message || '添加失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
alert('添加失败')
|
||||
} finally {
|
||||
setIsAdding(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 overflow-hidden">
|
||||
<Link href={`/components/${component.id}`}>
|
||||
<div className="relative h-48 bg-gray-100">
|
||||
{component.imageUrl ? (
|
||||
<Image
|
||||
src={component.imageUrl}
|
||||
alt={component.name}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-gray-400 text-4xl">📦</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">{component.brand}</div>
|
||||
<Link href={`/components/${component.id}`}>
|
||||
<h3 className="font-semibold text-lg mb-2 line-clamp-2 hover:text-blue-600 transition-colors">
|
||||
{component.name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
{component.description && (
|
||||
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
||||
{component.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
¥{component.price.toLocaleString()}
|
||||
</span>
|
||||
<span className={`text-sm px-2 py-1 rounded ${component.stock > 0
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{component.stock > 0 ? `库存 ${component.stock}` : '缺货'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={addToCart}
|
||||
disabled={component.stock <= 0 || isAdding}
|
||||
className={`w-full py-2 px-4 rounded-lg font-medium transition-colors flex items-center justify-center ${component.stock <= 0
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: isAdded
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{isAdding ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
) : isAdded ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
已添加
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
{component.stock <= 0 ? '缺货' : '加入购物车'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { User, ShoppingCart, Menu, X } from 'lucide-react'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
name?: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
// 检查用户登录状态
|
||||
const checkUserStatus = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
const userData = localStorage.getItem('user')
|
||||
if (userData) {
|
||||
setUser(JSON.parse(userData))
|
||||
}
|
||||
} else {
|
||||
setUser(null)
|
||||
}
|
||||
}
|
||||
|
||||
checkUserStatus()
|
||||
|
||||
// 监听localStorage变化
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'user' || e.key === 'token') {
|
||||
checkUserStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听自定义用户更新事件
|
||||
const handleUserUpdate = () => {
|
||||
checkUserStatus()
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
window.addEventListener('user-updated', handleUserUpdate)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
window.removeEventListener('user-updated', handleUserUpdate)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
setUser(null)
|
||||
// 触发用户更新事件
|
||||
window.dispatchEvent(new Event('user-updated'))
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 检查当前路径是否匹配
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/') {
|
||||
return pathname === '/'
|
||||
}
|
||||
return pathname.startsWith(path)
|
||||
}
|
||||
|
||||
// 导航链接样式
|
||||
const getLinkStyle = (path: string) => {
|
||||
return isActive(path)
|
||||
? 'text-blue-600 font-medium'
|
||||
: 'text-gray-700 hover:text-blue-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-md sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="text-2xl font-bold text-blue-600">
|
||||
PC DIY商城
|
||||
</Link> {/* Desktop Navigation */}
|
||||
<div className="hidden md:flex space-x-8">
|
||||
<Link href="/" className={getLinkStyle('/')}>
|
||||
首页
|
||||
</Link>
|
||||
<Link href="/components" className={getLinkStyle('/components')}>
|
||||
配件商城
|
||||
</Link>
|
||||
<Link href="/build" className={getLinkStyle('/build')}>
|
||||
装机配置
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* User Actions */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/cart" className="text-gray-700 hover:text-blue-600">
|
||||
<ShoppingCart className="h-6 w-6" />
|
||||
</Link>
|
||||
<div className="relative group">
|
||||
<button className="flex items-center space-x-2 text-gray-700 hover:text-blue-600">
|
||||
<User className="h-6 w-6" />
|
||||
<span>{user.name || user.username}</span>
|
||||
</button>
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||
<Link href="/profile" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
个人资料
|
||||
</Link>
|
||||
<Link href="/orders" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
我的订单
|
||||
</Link>
|
||||
{user.isAdmin && (
|
||||
<Link href="/admin" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
管理后台
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
登录
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
||||
>
|
||||
注册
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
{isMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
</button>
|
||||
</div>
|
||||
</div> {/* Mobile Navigation */}
|
||||
{isMenuOpen && (
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
<Link
|
||||
href="/"
|
||||
className={`block px-3 py-2 ${getLinkStyle('/')}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
首页
|
||||
</Link>
|
||||
<Link
|
||||
href="/components"
|
||||
className={`block px-3 py-2 ${getLinkStyle('/components')}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
配件商城
|
||||
</Link>
|
||||
<Link
|
||||
href="/build"
|
||||
className={`block px-3 py-2 ${getLinkStyle('/build')}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
装机配置
|
||||
</Link>
|
||||
{user ? (
|
||||
<>
|
||||
<Link
|
||||
href="/cart"
|
||||
className="block px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
购物车
|
||||
</Link>
|
||||
<Link
|
||||
href="/profile"
|
||||
className="block px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
个人资料
|
||||
</Link>
|
||||
<Link
|
||||
href="/orders"
|
||||
className="block px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
我的订单
|
||||
</Link>
|
||||
{user.isAdmin && (
|
||||
<Link
|
||||
href="/admin"
|
||||
className="block px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
管理后台
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block w-full text-left px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
className="block px-3 py-2 text-gray-700 hover:text-blue-600"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
登录
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="block px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
注册
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export function withAdminAuth<T extends object>(WrappedComponent: React.ComponentType<T>) {
|
||||
return function AdminProtectedComponent(props: T) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isAuthorized, setIsAuthorized] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const user = localStorage.getItem('user')
|
||||
|
||||
if (!token || !user) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(user)
|
||||
if (!userData.isAdmin) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
setIsAuthorized(true)
|
||||
} catch (error) {
|
||||
router.push('/login')
|
||||
return
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [router])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user