'use client' import { useState, useEffect } from 'react' import { withAdminAuth } from '@/components/admin/AdminAuth' import { Plus, Edit, Trash2, Search, Upload, Download, FileText } from 'lucide-react' interface Component { id: string name: string brand: string model: string price: number description?: string imageUrl?: string stock: number specifications?: string componentType: { id: string name: string } } interface ComponentType { id: string name: string } function AdminComponentsPage() { const [components, setComponents] = useState([]) const [componentTypes, setComponentTypes] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showModal, setShowModal] = useState(false) const [showBatchModal, setShowBatchModal] = useState(false) const [editingComponent, setEditingComponent] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [batchFile, setBatchFile] = useState(null) const [batchData, setBatchData] = useState([]) const [batchImportLoading, setBatchImportLoading] = useState(false) const [validationErrors, setValidationErrors] = useState([]) const [validData, setValidData] = useState([]) const [invalidData, setInvalidData] = useState([]) const [formData, setFormData] = useState({ name: '', brand: '', model: '', price: '', description: '', imageUrl: '', stock: '', componentTypeId: '', specifications: '' }) useEffect(() => { loadData() }, []) const loadData = async () => { try { const [componentsRes, typesRes] = await Promise.all([ fetch('/api/components?limit=100'), fetch('/api/component-types') ]) if (componentsRes.ok) { const data = await componentsRes.json() setComponents(data.components) } if (typesRes.ok) { const types = await typesRes.json() setComponentTypes(types) } } catch (error) { console.error('加载数据失败:', error) } finally { setIsLoading(false) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() try { const url = editingComponent ? `/api/components/${editingComponent.id}` : '/api/components' const method = editingComponent ? 'PUT' : 'POST' const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData), }) if (response.ok) { await loadData() setShowModal(false) resetForm() } else { const data = await response.json() alert(data.message || '操作失败') } } catch (error) { console.error('提交失败:', error) alert('操作失败') } } const handleDelete = async (id: string) => { if (!confirm('确定要删除这个配件吗?')) return try { const response = await fetch(`/api/components/${id}`, { method: 'DELETE', }) if (response.ok) { await loadData() } else { alert('删除失败') } } catch (error) { console.error('删除失败:', error) alert('删除失败') } } const handleEdit = (component: Component) => { setEditingComponent(component) setFormData({ name: component.name, brand: component.brand, model: component.model, price: component.price.toString(), description: component.description || '', imageUrl: component.imageUrl || '', stock: component.stock.toString(), componentTypeId: component.componentType.id, specifications: component.specifications || '' }) setShowModal(true) } const resetForm = () => { setFormData({ name: '', brand: '', model: '', price: '', description: '', imageUrl: '', stock: '', componentTypeId: '', specifications: '' }) setEditingComponent(null) } // 批量导入相关函数 const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { setBatchFile(file) parseFile(file) } } const parseFile = async (file: File) => { const text = await file.text() try { let data: any[] = [] if (file.name.endsWith('.json')) { const parsed = JSON.parse(text) data = Array.isArray(parsed) ? parsed : [parsed] } else if (file.name.endsWith('.csv')) { const lines = text.split('\n') if (lines.length < 2) { alert('CSV文件格式错误') return } const headers = lines[0].split(',').map(h => h.trim()) data = lines.slice(1) .filter(line => line.trim()) .map(line => { const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, '')) const obj: any = {} headers.forEach((header, index) => { obj[header] = values[index] || '' }) return obj }) } setBatchData(data) validateBatchData(data) } catch (error) { alert('文件解析失败,请检查文件格式') console.error('File parse error:', error) } } const validateBatchData = (data: any[]) => { const errors: string[] = [] const valid: any[] = [] const invalid: any[] = [] data.forEach((item, index) => { const itemErrors: string[] = [] // 验证必需字段 if (!item.name || typeof item.name !== 'string' || item.name.trim() === '') { itemErrors.push('缺少商品名称') } if (!item.brand || typeof item.brand !== 'string' || item.brand.trim() === '') { itemErrors.push('缺少品牌') } if (!item.model || typeof item.model !== 'string' || item.model.trim() === '') { itemErrors.push('缺少型号') } // 验证价格 const price = typeof item.price === 'string' ? parseFloat(item.price) : item.price if (isNaN(price) || price < 0) { itemErrors.push('价格必须是有效的正数') } // 验证库存 const stock = typeof item.stock === 'string' ? parseInt(item.stock) : item.stock if (isNaN(stock) || stock < 0) { itemErrors.push('库存必须是有效的非负整数') } // 验证配件类型 const hasTypeId = item.componentTypeId && item.componentTypeId.trim() !== '' const hasTypeName = (item.typeName || item.type) && (item.typeName || item.type).trim() !== '' if (!hasTypeId && !hasTypeName) { itemErrors.push('缺少配件类型信息 (componentTypeId, typeName 或 type)') } if (itemErrors.length > 0) { invalid.push({ ...item, _errors: itemErrors, _index: index + 1 }) errors.push(`第${index + 1}行: ${itemErrors.join(', ')}`) } else { valid.push(item) } }) setValidationErrors(errors) setValidData(valid) setInvalidData(invalid) } const handleBatchImport = async () => { if (validData.length === 0) { alert('没有有效的数据可导入') return } if (invalidData.length > 0) { const proceed = confirm(`发现${invalidData.length}行无效数据,是否只导入${validData.length}行有效数据?`) if (!proceed) return } setBatchImportLoading(true) try { const response = await fetch('/api/components/batch', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ components: validData }), }) const result = await response.json() if (response.ok && result.success) { const { summary, results } = result // 显示详细的导入结果 const failedItems = results.filter((r: any) => !r.success) let message = `导入完成!\n成功: ${summary.successful}\n失败: ${summary.failed}` if (summary.newTypesCreated > 0) { message += `\n新创建配件类型: ${summary.newTypesCreated}` } if (failedItems.length > 0 && failedItems.length <= 5) { message += '\n\n失败项目:' failedItems.forEach((item: any, index: number) => { message += `\n${index + 1}. ${item.item.name || '未知'}: ${item.error}` }) } else if (failedItems.length > 5) { message += `\n\n失败项目过多,请检查数据格式或查看控制台详情` console.log('批量导入失败项目:', failedItems) } alert(message) if (summary.successful > 0) { await loadData() setShowBatchModal(false) setBatchFile(null) setBatchData([]) setValidData([]) setInvalidData([]) setValidationErrors([]) } } else { alert(`批量导入失败: ${result.message || '未知错误'}`) console.error('Batch import error:', result) } } catch (error) { alert('批量导入失败:网络或服务器错误') console.error('Batch import error:', error) } finally { setBatchImportLoading(false) } } const downloadTemplate = (format: 'csv' | 'json') => { const sampleData = [ { name: 'Intel Core i5-13400F', brand: 'Intel', model: 'i5-13400F', price: 1299, description: '10核16线程,基础频率2.5GHz,最大睿频4.6GHz', imageUrl: '', stock: 50, typeName: 'CPU', specifications: '{"cores":10,"threads":16,"baseClock":"2.5GHz","boostClock":"4.6GHz","socket":"LGA1700"}' } ] if (format === 'csv') { const headers = ['name', 'brand', 'model', 'price', 'description', 'imageUrl', 'stock', 'typeName', 'specifications'] const csvContent = [ headers.join(','), ...sampleData.map(item => headers.map(header => `"${item[header as keyof typeof item] || ''}"`).join(',')) ].join('\n') const blob = new Blob([csvContent], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'components_template.csv' a.click() URL.revokeObjectURL(url) } else { const jsonContent = JSON.stringify(sampleData, null, 2) const blob = new Blob([jsonContent], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'components_template.json' a.click() URL.revokeObjectURL(url) } } const filteredComponents = components.filter(component => component.name.toLowerCase().includes(searchTerm.toLowerCase()) || component.brand.toLowerCase().includes(searchTerm.toLowerCase()) || component.model.toLowerCase().includes(searchTerm.toLowerCase()) ) if (isLoading) { return (
) } return (

配件管理

{/* Search */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" />
{/* Components Table */}
{filteredComponents.map((component) => ( ))}
配件信息 类型 价格 库存 操作
{component.imageUrl ? ( {component.name} ) : (
📦
)}
{component.name}
{component.brand} {component.model}
{component.componentType.name} ¥{component.price.toLocaleString()} 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }`}> {component.stock}
{/* Modal */} {showModal && (

{editingComponent ? '编辑配件' : '添加配件'}

setFormData({ ...formData, name: e.target.value })} required className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2" />
setFormData({ ...formData, brand: e.target.value })} required className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2" />
setFormData({ ...formData, model: e.target.value })} required className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2" />
setFormData({ ...formData, price: e.target.value })} required className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2" />
setFormData({ ...formData, stock: e.target.value })} required className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2" />