2025-06-24 14:09:12 +08:00

458 lines
18 KiB
TypeScript
Raw Permalink 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 { 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>
)
}