458 lines
18 KiB
TypeScript
458 lines
18 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import Link from 'next/link'
|
||
import { Check, Plus, Package, ShoppingCart, Cpu, HardDrive, MemoryStick, Zap, Monitor, Box, Search, X } from 'lucide-react'
|
||
|
||
interface ComponentType {
|
||
id: string
|
||
name: string
|
||
description?: string
|
||
}
|
||
|
||
interface Component {
|
||
id: string
|
||
name: string
|
||
brand: string
|
||
price: number
|
||
imageUrl?: string
|
||
componentType: ComponentType
|
||
}
|
||
|
||
interface BuildConfiguration {
|
||
[key: string]: Component | null
|
||
}
|
||
|
||
const componentIcons: { [key: string]: any } = {
|
||
'CPU': Cpu,
|
||
'内存': MemoryStick,
|
||
'硬盘': HardDrive,
|
||
'主板': Zap,
|
||
'显卡': Monitor,
|
||
'机箱': Box,
|
||
}
|
||
|
||
export default function BuildPage() {
|
||
const [componentTypes, setComponentTypes] = useState<ComponentType[]>([])
|
||
const [availableComponents, setAvailableComponents] = useState<{ [key: string]: Component[] }>({})
|
||
const [buildConfig, setBuildConfig] = useState<BuildConfiguration>({})
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [selectedType, setSelectedType] = useState<string | null>(null)
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
|
||
useEffect(() => {
|
||
loadData()
|
||
}, [])
|
||
|
||
const loadData = async () => {
|
||
try {
|
||
// 加载配件类型
|
||
const typesResponse = await fetch('/api/component-types')
|
||
if (typesResponse.ok) {
|
||
const types = await typesResponse.json()
|
||
setComponentTypes(types) // 为每种类型加载组件
|
||
const componentsData: { [key: string]: Component[] } = {}
|
||
for (const type of types) {
|
||
const componentsResponse = await fetch(`/api/components?type=${encodeURIComponent(type.id)}&limit=1000`)
|
||
if (componentsResponse.ok) {
|
||
const components = await componentsResponse.json()
|
||
componentsData[type.name] = components.components || []
|
||
}
|
||
}
|
||
setAvailableComponents(componentsData)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error)
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
const selectComponent = (typeName: string, component: Component) => {
|
||
setBuildConfig(prev => ({
|
||
...prev,
|
||
[typeName]: component
|
||
}))
|
||
setSelectedType(null)
|
||
setSearchTerm('')
|
||
}
|
||
|
||
const removeComponent = (typeName: string) => {
|
||
setBuildConfig(prev => ({
|
||
...prev,
|
||
[typeName]: null
|
||
}))
|
||
}
|
||
|
||
const getTotalPrice = () => {
|
||
return Object.values(buildConfig).reduce((total, component) => {
|
||
return total + (component?.price || 0)
|
||
}, 0)
|
||
}
|
||
|
||
const getCompletionRate = () => {
|
||
const totalTypes = componentTypes.length
|
||
const selectedTypes = Object.values(buildConfig).filter(Boolean).length
|
||
return totalTypes > 0 ? Math.round((selectedTypes / totalTypes) * 100) : 0
|
||
}
|
||
const addAllToCart = async () => {
|
||
const selectedComponents = Object.values(buildConfig).filter(Boolean)
|
||
if (selectedComponents.length === 0) {
|
||
alert('请先选择配件')
|
||
return
|
||
}
|
||
|
||
const token = localStorage.getItem('token')
|
||
if (!token) {
|
||
alert('请先登录')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 批量添加到购物车
|
||
for (const component of selectedComponents) {
|
||
await fetch('/api/cart', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify({
|
||
componentId: component!.id,
|
||
quantity: 1
|
||
})
|
||
})
|
||
}
|
||
|
||
window.dispatchEvent(new Event('cart-updated'))
|
||
alert('配置已添加到购物车!')
|
||
} catch (error) {
|
||
console.error('添加到购物车失败:', error)
|
||
alert('添加失败,请重试')
|
||
}
|
||
}
|
||
|
||
const createOrder = async () => {
|
||
const selectedComponents = Object.values(buildConfig).filter(Boolean)
|
||
if (selectedComponents.length !== componentTypes.length) {
|
||
alert('请完成所有配件的选择后再下单')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const user = JSON.parse(localStorage.getItem('user') || 'null')
|
||
if (!user) {
|
||
alert('请先登录')
|
||
window.location.href = '/login'
|
||
return
|
||
}
|
||
|
||
const orderItems = selectedComponents.map(component => ({
|
||
componentId: component!.id,
|
||
quantity: 1,
|
||
price: 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) {
|
||
alert('订单创建成功!')
|
||
setBuildConfig({})
|
||
window.location.href = '/orders'
|
||
} else {
|
||
alert('订单创建失败,请重试')
|
||
}
|
||
} catch (error) {
|
||
console.error('创建订单失败:', error)
|
||
alert('订单创建失败,请重试')
|
||
}
|
||
}
|
||
|
||
const getFilteredComponents = (typeName: string) => {
|
||
console.log(availableComponents, typeName);
|
||
|
||
const components = availableComponents[typeName] || []
|
||
if (!searchTerm) return components
|
||
|
||
return components.filter(component =>
|
||
component.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
component.brand.toLowerCase().includes(searchTerm.toLowerCase())
|
||
)
|
||
}
|
||
|
||
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>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Header */}
|
||
<div className="text-center mb-12">
|
||
<h1 className="text-4xl font-bold text-gray-900 mb-4">PC装机配置</h1>
|
||
<p className="text-xl text-gray-600 mb-8">选择每种配件,组装您的专属电脑</p>
|
||
|
||
{/* Progress */}
|
||
<div className="max-w-md mx-auto">
|
||
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||
<span>配置进度</span>
|
||
<span>{getCompletionRate()}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div
|
||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||
style={{ width: `${getCompletionRate()}%` }}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||
{/* Components Selection */}
|
||
<div className="lg:col-span-3">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{componentTypes.map((type) => {
|
||
const Icon = componentIcons[type.name] || Package
|
||
const selectedComponent = buildConfig[type.name]
|
||
|
||
return (
|
||
<div key={type.id} className="bg-white rounded-lg shadow-sm p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center space-x-3">
|
||
<Icon className="h-6 w-6 text-blue-600" />
|
||
<div>
|
||
<h3 className="font-semibold text-gray-900">{type.name}</h3>
|
||
<p className="text-sm text-gray-600">{type.description}</p>
|
||
</div>
|
||
</div>
|
||
{selectedComponent && (
|
||
<Check className="h-6 w-6 text-green-600" />
|
||
)}
|
||
</div>
|
||
|
||
{selectedComponent ? (
|
||
<div className="space-y-4">
|
||
<div className="border rounded-lg p-4 bg-green-50">
|
||
<div className="flex items-center space-x-4">
|
||
<div className="w-16 h-16 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||
{selectedComponent.imageUrl ? (
|
||
<img loading='lazy'
|
||
src={selectedComponent.imageUrl}
|
||
alt={selectedComponent.name}
|
||
className="max-w-full max-h-full object-contain"
|
||
/>
|
||
) : (
|
||
<Package className="h-8 w-8 text-gray-400" />
|
||
)}
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-medium text-gray-900">{selectedComponent.name}</h4>
|
||
<p className="text-sm text-gray-600">{selectedComponent.brand}</p>
|
||
<p className="text-lg font-bold text-red-600">¥{selectedComponent.price}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex space-x-2">
|
||
<button
|
||
onClick={() => setSelectedType(type.name)}
|
||
className="flex-1 text-blue-600 border border-blue-600 py-2 px-4 rounded-lg hover:bg-blue-50 transition-colors"
|
||
>
|
||
更换
|
||
</button>
|
||
<button
|
||
onClick={() => removeComponent(type.name)}
|
||
className="text-red-600 border border-red-600 py-2 px-4 rounded-lg hover:bg-red-50 transition-colors"
|
||
>
|
||
移除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => setSelectedType(type.name)}
|
||
className="w-full border-2 border-dashed border-gray-300 rounded-lg py-8 text-gray-500 hover:border-blue-300 hover:text-blue-600 transition-colors"
|
||
>
|
||
<Plus className="h-8 w-8 mx-auto mb-2" />
|
||
<p>选择 {type.name}</p>
|
||
</button>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 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">
|
||
{componentTypes.map((type) => {
|
||
const component = buildConfig[type.name]
|
||
return (
|
||
<div key={type.id} className="flex justify-between text-sm">
|
||
<span className="text-gray-600">{type.name}:</span>
|
||
<span className={component ? "text-gray-900 font-medium" : "text-gray-400"}>
|
||
{component ? `¥${component.price}` : '未选择'}
|
||
</span>
|
||
</div>
|
||
)
|
||
})}
|
||
|
||
<div className="border-t border-gray-200 pt-3">
|
||
<div className="flex justify-between font-semibold">
|
||
<span>总价:</span>
|
||
<span className="text-red-600">¥{getTotalPrice().toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<button
|
||
onClick={addAllToCart}
|
||
disabled={Object.values(buildConfig).filter(Boolean).length === 0}
|
||
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
<ShoppingCart className="h-5 w-5 inline mr-2" />
|
||
加入购物车
|
||
</button>
|
||
|
||
<button
|
||
onClick={createOrder}
|
||
disabled={getCompletionRate() < 100}
|
||
className="w-full border border-green-600 text-green-600 py-3 px-4 rounded-lg hover:bg-green-50 disabled:border-gray-300 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
直接下单
|
||
</button>
|
||
</div>
|
||
|
||
<p className="text-xs text-gray-500 mt-4">
|
||
完成度: {getCompletionRate()}% ({Object.values(buildConfig).filter(Boolean).length}/{componentTypes.length})
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Component Selection Modal */}
|
||
{selectedType && (
|
||
<div className="fixed inset-0 bg-opacity-30 flex items-center justify-center p-4 z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}> <div className="bg-white rounded-xl max-w-5xl w-full max-h-[85vh] overflow-hidden shadow-2xl">
|
||
<div className="p-6 border-b border-gray-200 bg-gray-50">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-xl font-semibold text-gray-900">选择 {selectedType}</h3>
|
||
<button
|
||
onClick={() => {
|
||
setSelectedType(null)
|
||
setSearchTerm('')
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600 p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search Box */}
|
||
<div className="relative">
|
||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<Search className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="text"
|
||
placeholder={`搜索${selectedType}配件...`}
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||
/>
|
||
{searchTerm && (
|
||
<button
|
||
onClick={() => setSearchTerm('')}
|
||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||
>
|
||
<X className="h-4 w-4 text-gray-400 hover:text-gray-600" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Results count */}
|
||
<div className="mt-3 text-sm text-gray-600">
|
||
找到 {getFilteredComponents(selectedType).length} 个配件
|
||
{searchTerm && ` (搜索: "${searchTerm}")`}
|
||
</div>
|
||
</div>
|
||
<div className="p-6 overflow-y-auto" style={{ maxHeight: 'calc(85vh - 200px)' }}>
|
||
{getFilteredComponents(selectedType).length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{getFilteredComponents(selectedType).map((component) => (
|
||
<div
|
||
key={component.id}
|
||
className="border border-gray-200 rounded-lg p-5 hover:shadow-lg hover:border-blue-300 cursor-pointer transition-all duration-200 bg-white"
|
||
onClick={() => selectComponent(selectedType, component)}
|
||
>
|
||
<div className="w-full h-36 bg-gray-50 rounded-lg mb-4 flex items-center justify-center overflow-hidden">
|
||
{component.imageUrl ? (
|
||
<img loading='lazy'
|
||
src={component.imageUrl}
|
||
alt={component.name}
|
||
className="max-w-full max-h-full object-contain"
|
||
/>
|
||
) : (
|
||
<Package className="h-12 w-12 text-gray-400" />
|
||
)}
|
||
</div>
|
||
<h4 className="font-medium text-gray-900 mb-2 line-clamp-2 text-sm">
|
||
{component.name}
|
||
</h4>
|
||
<p className="text-sm text-gray-600 mb-3">{component.brand}</p>
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-lg font-bold text-red-600">¥{component.price}</p>
|
||
<button className="text-blue-600 hover:text-blue-800 text-sm font-medium">
|
||
选择
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-12">
|
||
<Package className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
{searchTerm ? '没有找到匹配的配件' : '暂无配件'}
|
||
</h3>
|
||
<p className="text-gray-500">
|
||
{searchTerm ? '尝试调整搜索关键词' : '此类型暂无可用配件'}
|
||
</p>
|
||
{searchTerm && (
|
||
<button
|
||
onClick={() => setSearchTerm('')}
|
||
className="mt-4 text-blue-600 hover:text-blue-800 font-medium"
|
||
>
|
||
清除搜索
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|