实现了AI助手

This commit is contained in:
2025-06-24 14:09:12 +08:00
parent 7d139b22db
commit 8a4ccd5ef2
31 changed files with 2883 additions and 252 deletions
+75 -67
View File
@@ -51,15 +51,29 @@ function AdminComponentsPage() {
specifications: ''
})
// Helper function to get authenticated headers
const getAuthHeaders = (): HeadersInit => {
const token = localStorage.getItem('token')
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
try {
const headers = getAuthHeaders()
const [componentsRes, typesRes] = await Promise.all([
fetch('/api/components?limit=100'),
fetch('/api/component-types')
fetch('/api/admin/components?limit=100', { headers }),
fetch('/api/component-types', { headers })
])
if (componentsRes.ok) {
@@ -77,22 +91,19 @@ function AdminComponentsPage() {
setIsLoading(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const url = editingComponent
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',
},
headers: getAuthHeaders(),
body: JSON.stringify(formData),
})
@@ -109,13 +120,13 @@ function AdminComponentsPage() {
alert('操作失败')
}
}
const handleDelete = async (id: string) => {
if (!confirm('确定要删除这个配件吗?')) return
try {
const response = await fetch(`/api/components/${id}`, {
method: 'DELETE',
headers: getAuthHeaders(),
})
if (response.ok) {
@@ -170,10 +181,10 @@ function AdminComponentsPage() {
}
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]
@@ -183,7 +194,7 @@ function AdminComponentsPage() {
alert('CSV文件格式错误')
return
}
const headers = lines[0].split(',').map(h => h.trim())
data = lines.slice(1)
.filter(line => line.trim())
@@ -196,7 +207,7 @@ function AdminComponentsPage() {
return obj
})
}
setBatchData(data)
validateBatchData(data)
} catch (error) {
@@ -212,7 +223,7 @@ function AdminComponentsPage() {
data.forEach((item, index) => {
const itemErrors: string[] = []
// 验证必需字段
if (!item.name || typeof item.name !== 'string' || item.name.trim() === '') {
itemErrors.push('缺少商品名称')
@@ -223,26 +234,26 @@ function AdminComponentsPage() {
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(', ')}`)
@@ -268,31 +279,29 @@ function AdminComponentsPage() {
}
setBatchImportLoading(true)
try {
const response = await fetch('/api/components/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: getAuthHeaders(),
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) => {
@@ -302,9 +311,9 @@ function AdminComponentsPage() {
message += `\n\n失败项目过多,请检查数据格式或查看控制台详情`
console.log('批量导入失败项目:', failedItems)
}
alert(message)
if (summary.successful > 0) {
await loadData()
setShowBatchModal(false)
@@ -318,7 +327,7 @@ function AdminComponentsPage() {
alert(`批量导入失败: ${result.message || '未知错误'}`)
console.error('Batch import error:', result)
}
} catch (error) {
alert('批量导入失败:网络或服务器错误')
console.error('Batch import error:', error)
@@ -348,7 +357,7 @@ function AdminComponentsPage() {
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')
@@ -383,27 +392,27 @@ function AdminComponentsPage() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">配件管理</h1>
<div className="flex gap-3">
<button
onClick={() => setShowBatchModal(true)}
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 flex items-center gap-2"
>
<Upload className="h-5 w-5" />
批量导入
</button>
<button
onClick={() => {
resetForm()
setShowModal(true)
}}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="h-5 w-5" />
添加配件
</button>
</div>
<h1 className="text-3xl font-bold text-gray-900">配件管理</h1>
<div className="flex gap-3">
<button
onClick={() => setShowBatchModal(true)}
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 flex items-center gap-2"
>
<Upload className="h-5 w-5" />
批量导入
</button>
<button
onClick={() => {
resetForm()
setShowModal(true)
}}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="h-5 w-5" />
添加配件
</button>
</div>
</div>
{/* Search */}
<div className="mb-6">
@@ -444,7 +453,7 @@ function AdminComponentsPage() {
<tbody className="bg-white divide-y divide-gray-200">
{filteredComponents.map((component) => (
<tr key={component.id}>
<td className="px-6 py-4 whitespace-nowrap">
<td className="px-6 py-4 max-w-md">
<div className="flex items-center">
<div className="h-10 w-10 flex-shrink-0">
{component.imageUrl ? (
@@ -459,11 +468,11 @@ function AdminComponentsPage() {
</div>
)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
<div className="ml-4 min-w-0 flex-1">
<div className="text-sm font-medium text-gray-900 truncate">
{component.name}
</div>
<div className="text-sm text-gray-500">
<div className="text-sm text-gray-500 truncate">
{component.brand} {component.model}
</div>
</div>
@@ -476,11 +485,10 @@ function AdminComponentsPage() {
¥{component.price.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs rounded-full ${
component.stock > 0
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
<span className={`inline-flex px-2 py-1 text-xs rounded-full ${component.stock > 0
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{component.stock}
</span>
</td>
@@ -506,12 +514,12 @@ function AdminComponentsPage() {
{/* Modal */}
{showModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
<div className="fixed inset-0 flex items-center justify-center z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
<div className="bg-white rounded-lg p-6 w-full max-w-md max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">
{editingComponent ? '编辑配件' : '添加配件'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">配件类型</label>
@@ -628,10 +636,10 @@ function AdminComponentsPage() {
{/* Batch Import Modal */}
{showBatchModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
<div className="fixed inset-0 flex items-center justify-center z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
<div className="bg-white rounded-lg p-6 w-full max-w-md max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">批量导入配件</h2>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">选择文件</label>
<input
@@ -650,7 +658,7 @@ function AdminComponentsPage() {
</div>
)}
<div className="mb-4">
<div className="mb-8 flex gap-2">
<button
onClick={() => downloadTemplate('csv')}
className="mr-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 flex items-center gap-2"
+1 -1
View File
@@ -203,7 +203,7 @@ function AdminOrdersPage() {
{/* Order Details Modal */}
{showModal && selectedOrder && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
<div className="fixed inset-0 flex items-center justify-center z-50" style={{backgroundColor: 'rgba(0, 0, 0, 0.5)'}}>
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">订单详情</h2>