2 Commits

4 changed files with 604 additions and 47 deletions

162
app.py
View File

@@ -523,6 +523,168 @@ def api_parse_images():
# ============ 智能添加API ============
# ============ 智能补充参数API ============
@app.route('/api/models/<model_id>/smart-update', methods=['POST'])
def api_smart_update_model(model_id):
"""智能补充模型参数(只填充缺失字段)"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text and not images:
return jsonify({'error': '文本或图片不能都为空'}), 400
# 获取现有数据
models = load_data(MODELS_FILE)
model = next((m for m in models if m['id'] == model_id), None)
if not model:
return jsonify({'error': 'Model not found'}), 404
# 解析新参数
parsed_list = parse_with_llm(text, 'model', images)
if not parsed_list:
return jsonify({'error': '解析失败'}), 500
parsed = parsed_list[0] # 补充只取第一个
# 只填充缺失或为空的字段
updated_fields = []
for key, value in parsed.items():
if value is not None and value != '' and value != 0:
existing = model.get(key)
if existing is None or existing == '' or existing == 0:
model[key] = value
updated_fields.append(key)
model['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
model['raw_text'] = model.get('raw_text', '') + '\n' + text if text else model.get('raw_text', '')
if images:
existing_images = model.get('images', [])
model['images'] = existing_images + images
save_data(MODELS_FILE, models)
return jsonify({'success': True, 'updated_fields': updated_fields, 'model': model})
@app.route('/api/gpus/<gpu_id>/smart-update', methods=['POST'])
def api_smart_update_gpu(gpu_id):
"""智能补充GPU参数只填充缺失字段"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text and not images:
return jsonify({'error': '文本或图片不能都为空'}), 400
gpus = load_data(GPUS_FILE)
gpu = next((g for g in gpus if g['id'] == gpu_id), None)
if not gpu:
return jsonify({'error': 'GPU not found'}), 404
parsed_list = parse_with_llm(text, 'gpu', images)
if not parsed_list:
return jsonify({'error': '解析失败'}), 500
parsed = parsed_list[0]
updated_fields = []
for key, value in parsed.items():
if value is not None and value != '' and value != 0:
existing = gpu.get(key)
if existing is None or existing == '' or existing == 0:
gpu[key] = value
updated_fields.append(key)
gpu['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
gpu['raw_text'] = gpu.get('raw_text', '') + '\n' + text if text else gpu.get('raw_text', '')
if images:
existing_images = gpu.get('images', [])
gpu['images'] = existing_images + images
save_data(GPUS_FILE, gpus)
return jsonify({'success': True, 'updated_fields': updated_fields, 'gpu': gpu})
@app.route('/api/cpus/<cpu_id>/smart-update', methods=['POST'])
def api_smart_update_cpu(cpu_id):
"""智能补充CPU参数只填充缺失字段"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text and not images:
return jsonify({'error': '文本或图片不能都为空'}), 400
cpus = load_data(CPUS_FILE)
cpu = next((c for c in cpus if c['id'] == cpu_id), None)
if not cpu:
return jsonify({'error': 'CPU not found'}), 404
parsed_list = parse_with_llm(text, 'cpu', images)
if not parsed_list:
return jsonify({'error': '解析失败'}), 500
parsed = parsed_list[0]
updated_fields = []
for key, value in parsed.items():
if value is not None and value != '' and value != 0:
existing = cpu.get(key)
if existing is None or existing == '' or existing == 0:
cpu[key] = value
updated_fields.append(key)
cpu['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cpu['raw_text'] = cpu.get('raw_text', '') + '\n' + text if text else cpu.get('raw_text', '')
if images:
existing_images = cpu.get('images', [])
cpu['images'] = existing_images + images
save_data(CPUS_FILE, cpus)
return jsonify({'success': True, 'updated_fields': updated_fields, 'cpu': cpu})
@app.route('/api/items/<category_id>/<item_id>/smart-update', methods=['POST'])
def api_smart_update_item(category_id, item_id):
"""智能补充动态分类数据参数(只填充缺失字段)"""
data = request.get_json()
text = data.get('text', '')
images = data.get('images', [])
if not text and not images:
return jsonify({'error': '文本或图片不能都为空'}), 400
items_file = DATA_DIR / f'items_{category_id}.json'
items = load_data(items_file)
item = next((i for i in items if i['id'] == item_id), None)
if not item:
return jsonify({'error': 'Item not found'}), 404
parsed_list = parse_with_llm(text, 'dynamic', images)
if not parsed_list:
return jsonify({'error': '解析失败'}), 500
parsed = parsed_list[0]
updated_fields = []
for key, value in parsed.items():
if value is not None and value != '' and value != 0:
existing = item.get(key)
if existing is None or existing == '' or existing == 0:
item[key] = value
updated_fields.append(key)
item['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
item['raw_text'] = item.get('raw_text', '') + '\n' + text if text else item.get('raw_text', '')
if images:
existing_images = item.get('images', [])
item['images'] = existing_images + images
save_data(items_file, items)
return jsonify({'success': True, 'updated_fields': updated_fields, 'item': item})
@app.route('/api/models/smart-add', methods=['POST'])
def api_smart_add_model():
"""智能添加模型(支持文本和多图解析,可能添加多个产品)"""

View File

@@ -11,7 +11,12 @@
"id": "chat",
"name": "对话模型",
"icon": "ri-chat-3-line",
"key_features": ["context_length", "mmlu", "input_price", "output_price"],
"key_features": [
"context_length",
"mmlu",
"input_price",
"output_price"
],
"feature_labels": {
"context_length": "上下文",
"mmlu": "MMLU",
@@ -23,7 +28,11 @@
"id": "code",
"name": "代码模型",
"icon": "ri-code-line",
"key_features": ["humaneval", "context_length", "input_price"],
"key_features": [
"humaneval",
"context_length",
"input_price"
],
"feature_labels": {
"humaneval": "HumanEval",
"context_length": "上下文",
@@ -34,7 +43,11 @@
"id": "reasoning",
"name": "推理模型",
"icon": "ri-lightbulb-line",
"key_features": ["reasoning_capability", "mmlu", "context_length"],
"key_features": [
"reasoning_capability",
"mmlu",
"context_length"
],
"feature_labels": {
"reasoning_capability": "推理能力",
"mmlu": "MMLU",
@@ -45,7 +58,11 @@
"id": "vision",
"name": "视觉模型",
"icon": "ri-image-line",
"key_features": ["vision_capability", "multimodal", "context_length"],
"key_features": [
"vision_capability",
"multimodal",
"context_length"
],
"feature_labels": {
"vision_capability": "视觉能力",
"multimodal": "多模态",
@@ -66,7 +83,12 @@
"id": "gaming",
"name": "游戏显卡",
"icon": "ri-gamepad-line",
"key_features": ["memory_gb", "cuda_cores", "price_usd", "fp16_tflops"],
"key_features": [
"memory_gb",
"cuda_cores",
"price_usd",
"fp16_tflops"
],
"feature_labels": {
"memory_gb": "显存",
"cuda_cores": "CUDA核心",
@@ -78,7 +100,12 @@
"id": "professional",
"name": "专业显卡",
"icon": "ri-building-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "price_usd"],
"key_features": [
"memory_gb",
"tensor_cores",
"memory_bandwidth_gbs",
"price_usd"
],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
@@ -90,7 +117,12 @@
"id": "datacenter",
"name": "数据中心",
"icon": "ri-server-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "fp16_tflops"],
"key_features": [
"memory_gb",
"tensor_cores",
"memory_bandwidth_gbs",
"fp16_tflops"
],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
@@ -112,7 +144,12 @@
"id": "desktop",
"name": "桌面CPU",
"icon": "ri-computer-line",
"key_features": ["cores", "threads", "boost_clock_ghz", "price_usd"],
"key_features": [
"cores",
"threads",
"boost_clock_ghz",
"price_usd"
],
"feature_labels": {
"cores": "核心",
"threads": "线程",
@@ -124,7 +161,12 @@
"id": "server",
"name": "服务器CPU",
"icon": "ri-server-line",
"key_features": ["cores", "threads", "l3_cache_mb", "tdp_watts"],
"key_features": [
"cores",
"threads",
"l3_cache_mb",
"tdp_watts"
],
"feature_labels": {
"cores": "核心",
"threads": "线程",
@@ -136,7 +178,12 @@
"id": "mobile",
"name": "移动CPU",
"icon": "ri-smartphone-line",
"key_features": ["cores", "threads", "base_clock_ghz", "tdp_watts"],
"key_features": [
"cores",
"threads",
"base_clock_ghz",
"tdp_watts"
],
"feature_labels": {
"cores": "核心",
"threads": "线程",
@@ -159,7 +206,12 @@
"id": "flagship",
"name": "旗舰手机",
"icon": "ri-star-line",
"key_features": ["processor", "ram_gb", "storage_gb", "price"],
"key_features": [
"processor",
"ram_gb",
"storage_gb",
"price"
],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
@@ -171,7 +223,12 @@
"id": "midrange",
"name": "中端手机",
"icon": "ri-price-tag-3-line",
"key_features": ["processor", "ram_gb", "battery_mah", "price"],
"key_features": [
"processor",
"ram_gb",
"battery_mah",
"price"
],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
@@ -193,7 +250,12 @@
"id": "gaming-laptop",
"name": "游戏笔记本",
"icon": "ri-gamepad-line",
"key_features": ["processor", "gpu", "ram_gb", "price"],
"key_features": [
"processor",
"gpu",
"ram_gb",
"price"
],
"feature_labels": {
"processor": "处理器",
"gpu": "显卡",
@@ -205,7 +267,12 @@
"id": "business-laptop",
"name": "商务笔记本",
"icon": "ri-briefcase-line",
"key_features": ["processor", "ram_gb", "weight_kg", "price"],
"key_features": [
"processor",
"ram_gb",
"weight_kg",
"price"
],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
@@ -228,7 +295,11 @@
"id": "sedan",
"name": "轿车",
"icon": "ri-car-line",
"key_features": ["engine", "power_kw", "price"],
"key_features": [
"engine",
"power_kw",
"price"
],
"feature_labels": {
"engine": "发动机",
"power_kw": "功率",
@@ -239,7 +310,11 @@
"id": "suv",
"name": "SUV",
"icon": "ri-truck-line",
"key_features": ["engine", "seats", "price"],
"key_features": [
"engine",
"seats",
"price"
],
"feature_labels": {
"engine": "发动机",
"seats": "座位数",
@@ -253,35 +328,46 @@
"name": "摄像",
"icon": "ri-camera-line",
"color": "blue",
"order": 0,
"order": 9,
"visible": true,
"description": "相机、摄像机等",
"created_at": "2026-04-25 16:38:47",
"subcategories": [
{
"id": "mirrorless",
"name": "无反相机",
"icon": "ri-camera-line",
"key_features": ["sensor", "megapixels", "video_resolution", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"video_resolution": "视频",
"price": "价格"
}
"price": "价格",
"sensor": "传感器",
"video_resolution": "视频"
},
"icon": "ri-camera-line",
"id": "mirrorless",
"key_features": [
"sensor",
"megapixels",
"video_resolution",
"price"
],
"name": "无反相机"
},
{
"id": "dslr",
"name": "单反相机",
"icon": "ri-camera-2-line",
"key_features": ["sensor", "megapixels", "lens_mount", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"lens_mount": "卡口",
"price": "价格"
}
"megapixels": "像素",
"price": "价格",
"sensor": "传感器"
},
"icon": "ri-camera-2-line",
"id": "dslr",
"key_features": [
"sensor",
"megapixels",
"lens_mount",
"price"
],
"name": "单反相机"
}
]
],
"updated_at": "2026-04-28 09:44:52"
}
]

View File

@@ -13,7 +13,9 @@
"is_open_source": false,
"license": "Proprietary",
"description": "OpenAI最强大的多模态大模型",
"created_at": "2024-01-01"
"created_at": "2024-01-01",
"updated_at": "2026-04-28 10:09:47",
"raw_text": "\nGPT-4 Turbo version with 128K context length, price is $10 per 1M input tokens"
},
{
"id": "gpt4turbo",

View File

@@ -130,6 +130,9 @@
<h1 class="text-2xl font-bold text-gray-800">分类管理</h1>
<button onclick="openAddModal('category')" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-add-line mr-2"></i>添加分类</button>
</div>
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>内置分类AI模型、GPU、CPU的子类别配置可在此编辑其数据管理入口在左侧导航栏的独立页面。</p>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
@@ -137,8 +140,8 @@
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">图标</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">名称</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">描述</th>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">显示</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">类型</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">子类别</th>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
@@ -299,6 +302,7 @@
<div id="modalContent" class="p-6"></div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="openSmartUpdateModal()" id="smartUpdateBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 hidden"><i class="ri-magic-line mr-1"></i>智能补充</button>
<button onclick="saveItem()" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">保存</button>
</div>
</div>
@@ -386,6 +390,57 @@
</div>
</div>
<!-- 智能补充弹窗 -->
<div id="smartUpdateModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-auto">
<div class="p-6 border-b flex justify-between items-center sticky top-0 bg-white z-10">
<h2 class="text-xl font-bold text-gray-800"><i class="ri-magic-line mr-2 text-orange-600"></i>智能补充参数</h2>
<button onclick="closeSmartUpdateModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div class="p-6">
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>上传图片或输入文本AI将识别参数并补充到现有数据中。只会填充缺失的字段不会覆盖已有值。</p>
</div>
<div class="mb-6">
<p class="text-sm text-gray-500 mb-3">上传产品图片AI将自动识别并解析参数</p>
<div class="flex flex-wrap gap-3 mb-3" id="smartUpdateImagePreviewArea">
<!-- 图片预览区 -->
</div>
<div class="flex gap-3">
<input type="file" id="smartUpdateImageInput" accept="image/*" multiple class="hidden" onchange="handleSmartUpdateImageUpload(event)">
<button onclick="document.getElementById('smartUpdateImageInput').click()" class="px-4 py-2 bg-orange-100 text-orange-600 rounded-lg hover:bg-orange-200 text-sm">
<i class="ri-image-add-line mr-1"></i>选择图片(支持多选)
</button>
<button onclick="pasteSmartUpdateImageFromClipboard()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-clipboard-line mr-1"></i>粘贴图片
</button>
<button onclick="clearSmartUpdateImages()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-delete-bin-line mr-1"></i>清空图片
</button>
</div>
<div class="mt-3 text-xs text-gray-400">
<i class="ri-information-line mr-1"></i>
已选择 <span id="smartUpdateImageCount">0</span> 张图片
</div>
</div>
<div class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block">补充文本(可选)</label>
<textarea id="smartUpdateText" rows="4" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="可粘贴补充信息文本,如产品规格表、参数说明等..."></textarea>
</div>
<div id="smartUpdatePreview" class="mt-4 hidden">
<h3 class="text-sm font-semibold text-gray-700 mb-2"><i class="ri-checkbox-circle-line text-green-600 mr-1"></i>解析结果:</h3>
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartUpdateResult">
<!-- 解析结果显示 -->
</div>
</div>
</div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeSmartUpdateModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="smartUpdateSubmit()" id="smartUpdateSubmitBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-1"></i>解析并补充</button>
</div>
</div>
</div>
<script>
let currentType = '';
let currentId = '';
@@ -702,6 +757,9 @@
: '<div class="text-gray-400">暂无数据</div>';
}
// 内置分类列表
const builtinCategories = ['ai-models', 'gpus', 'cpus'];
// 加载分类列表
async function loadAdminCategories() {
const res = await fetch('/api/categories?all=1');
@@ -712,23 +770,27 @@
return;
}
document.getElementById('admin-categories-table').innerHTML = categories.map(c => `
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''}">
document.getElementById('admin-categories-table').innerHTML = categories.map(c => {
const isBuiltin = builtinCategories.includes(c.id);
const subcatCount = (c.subcategories || []).length;
return `
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''} ${isBuiltin ? 'bg-indigo-50' : ''}">
<td class="px-4 py-3"><div class="w-10 h-10 rounded-lg ${colorMap[c.color] || 'bg-gray-100 text-gray-600'} flex items-center justify-center"><i class="${c.icon} text-xl"></i></div></td>
<td class="px-4 py-3 text-gray-500 text-sm font-mono">${c.id}</td>
<td class="px-4 py-3 font-medium text-gray-800">${c.name}</td>
<td class="px-4 py-3 text-gray-600 text-sm">${c.description || '-'}</td>
<td class="px-4 py-3 text-center">
<button onclick="toggleVisible('category', '${c.id}')" class="${c.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${c.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${c.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
<td class="px-4 py-3 text-sm">
${isBuiltin ? '<span class="px-2 py-1 bg-indigo-100 text-indigo-600 rounded text-xs">内置</span>' : '<span class="text-gray-500">自定义</span>'}
</td>
<td class="px-4 py-3 text-sm">
${subcatCount > 0 ? `<span class="px-2 py-1 bg-green-100 text-green-600 rounded text-xs">${subcatCount} 个</span>` : '<span class="text-gray-400">无</span>'}
</td>
<td class="px-4 py-3 text-center">
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('category', '${c.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2" title="编辑子类别"><i class="ri-edit-line"></i></button>
${!isBuiltin ? `<button onclick="deleteItem('category', '${c.id}')" class="text-red-600 hover:text-red-800" title="删除"><i class="ri-delete-bin-line"></i></button>` : '<span class="text-gray-300 cursor-not-allowed"><i class="ri-delete-bin-line"></i></span>'}
</td>
</tr>
`).join('');
`;
}).join('');
}
// 加载模型列表
@@ -880,6 +942,7 @@
document.getElementById('modalTitle').textContent = '添加' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm, dynamic: getDynamicForm};
document.getElementById('modalContent').innerHTML = forms[type]();
showSmartUpdateButton(); // 添加时隐藏智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
@@ -894,6 +957,7 @@
document.getElementById('modalTitle').textContent = '编辑' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm};
document.getElementById('modalContent').innerHTML = forms[type](currentData);
showSmartUpdateButton(); // 显示智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
@@ -905,6 +969,7 @@
currentData = await res.json();
document.getElementById('modalTitle').textContent = '编辑数据';
document.getElementById('modalContent').innerHTML = getDynamicForm(currentData);
showSmartUpdateButton(); // 显示智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
@@ -1121,9 +1186,47 @@
// 表单模板
function getCategoryForm(data = {}) {
const subcategories = data.subcategories || [];
const isBuiltin = builtinCategories.includes(data.id);
// 存储到全局变量,便于管理
window.currentEditingSubcategories = JSON.parse(JSON.stringify(subcategories));
// 内置类别只显示子类别管理
if (isBuiltin) {
return `<form id="itemForm" class="space-y-4">
<div class="bg-indigo-50 rounded-lg p-4 mb-4">
<p class="text-sm text-indigo-700"><i class="ri-information-line mr-1"></i>内置分类的基础信息不可修改,只可编辑子类别配置。</p>
</div>
<div class="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
<div><label class="text-sm text-gray-500 mb-1 block">ID</label><div class="text-gray-700 font-mono">${data.id}</div></div>
<div><label class="text-sm text-gray-500 mb-1 block">名称</label><div class="text-gray-700">${data.name}</div></div>
<div><label class="text-sm text-gray-500 mb-1 block">图标</label><div class="text-gray-700"><i class="${data.icon} mr-1"></i>${data.icon}</div></div>
<div><label class="text-sm text-gray-500 mb-1 block">颜色</label><div class="text-gray-700">${data.color}</div></div>
</div>
<input type="hidden" name="id" value="${data.id}">
<input type="hidden" name="name" value="${data.name}">
<input type="hidden" name="icon" value="${data.icon}">
<input type="hidden" name="color" value="${data.color}">
<input type="hidden" name="order" value="${data.order || 0}">
<input type="hidden" name="visible" value="${data.visible !== false ? 'true' : 'false'}">
<input type="hidden" name="description" value="${data.description || ''}">
<!-- 子类别管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-700"><i class="ri-folder-line mr-1"></i>子类别管理</label>
<button onclick="openSubcategoryAddModal()" class="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700">
<i class="ri-add-line mr-1"></i>添加子类别
</button>
</div>
<div id="subcategoriesList" class="space-y-2">
${renderSubcategoriesList(subcategories)}
</div>
<input type="hidden" name="subcategories" id="subcategoriesHidden" value='${JSON.stringify(subcategories)}'>
</div>
</form>`;
}
// 自定义类别完整编辑表单
return `<form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div><label class="text-sm text-gray-600 mb-1 block">ID *</label><input type="text" name="id" value="${data.id || ''}" ${data.id ? 'readonly' : ''} required class="w-full px-3 py-2 border rounded-lg ${data.id ? 'bg-gray-100' : ''}"></div>
@@ -1413,6 +1516,7 @@
document.getElementById('smartAddModal').addEventListener('click', function(e) { if (e.target === this) closeSmartAddModal(); });
document.getElementById('rawDataModal').addEventListener('click', function(e) { if (e.target === this) closeRawDataModal(); });
document.getElementById('subcategoryModal').addEventListener('click', function(e) { if (e.target === this) closeSubcategoryModal(); });
document.getElementById('smartUpdateModal').addEventListener('click', function(e) { if (e.target === this) closeSmartUpdateModal(); });
// ============ 智能添加功能 ============
@@ -1741,6 +1845,209 @@
document.getElementById('rawDataModal').classList.add('hidden');
}
// ============ 智能补充功能 ============
let smartUpdateImages = [];
// 打开智能补充弹框(编辑时显示按钮)
function showSmartUpdateButton() {
const btn = document.getElementById('smartUpdateBtn');
// 只有编辑已有数据时才显示智能补充按钮排除category和knowledge
if (currentId && currentType && currentType !== 'category' && currentType !== 'knowledge') {
btn.classList.remove('hidden');
} else {
btn.classList.add('hidden');
}
}
// 打开智能补充弹框
function openSmartUpdateModal() {
smartUpdateImages = [];
document.getElementById('smartUpdateText').value = '';
document.getElementById('smartUpdatePreview').classList.add('hidden');
document.getElementById('smartUpdateImagePreviewArea').innerHTML = '';
document.getElementById('smartUpdateImageCount').textContent = '0';
document.getElementById('smartUpdateModal').classList.remove('hidden');
}
function closeSmartUpdateModal() {
document.getElementById('smartUpdateModal').classList.add('hidden');
}
// 处理图片上传
async function handleSmartUpdateImageUpload(event) {
const files = event.target.files;
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
smartUpdateImages.push(data.url);
updateSmartUpdateImagePreview();
}
} catch (e) {
alert('上传失败: ' + e.message);
}
}
event.target.value = '';
}
// 粘贴图片
async function pasteSmartUpdateImageFromClipboard() {
try {
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
found = true;
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartUpdateImages.push(data.url);
updateSmartUpdateImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
// 清空图片
function clearSmartUpdateImages() {
smartUpdateImages = [];
updateSmartUpdateImagePreview();
}
// 移除单张图片
function removeSmartUpdateImage(index) {
smartUpdateImages.splice(index, 1);
updateSmartUpdateImagePreview();
}
// 更新图片预览
function updateSmartUpdateImagePreview() {
const area = document.getElementById('smartUpdateImagePreviewArea');
const count = document.getElementById('smartUpdateImageCount');
area.innerHTML = smartUpdateImages.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeSmartUpdateImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('');
count.textContent = smartUpdateImages.length;
}
// 执行智能补充
async function smartUpdateSubmit() {
const text = document.getElementById('smartUpdateText').value.trim();
if (!text && smartUpdateImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('smartUpdateSubmitBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
try {
let endpoint;
if (currentType === 'model') endpoint = `/api/models/${currentId}/smart-update`;
else if (currentType === 'gpu') endpoint = `/api/gpus/${currentId}/smart-update`;
else if (currentType === 'cpu') endpoint = `/api/cpus/${currentId}/smart-update`;
else if (currentType === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${currentId}/smart-update`;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
images: smartUpdateImages
})
});
const data = await res.json();
if (data.error) {
alert('解析失败: ' + data.error);
} else {
// 显示更新结果
document.getElementById('smartUpdatePreview').classList.remove('hidden');
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-fill mr-1"></i>成功补充 ${data.updated_fields.length} 个字段</div>`;
if (data.updated_fields.length > 0) {
html += '<div class="mt-2">';
data.updated_fields.forEach(field => {
html += `<span class="inline-block px-2 py-1 bg-green-100 text-green-700 rounded text-xs mr-1 mb-1">${field}</span>`;
});
html += '</div>';
} else {
html += '<div class="text-gray-500 mt-2">所有字段都已存在,无需补充</div>';
}
document.getElementById('smartUpdateResult').innerHTML = html;
// 更新编辑表单数据
currentData = data[currentType] || data.item || data.model || data.gpu || data.cpu;
const forms = {model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, dynamic: getDynamicForm};
document.getElementById('modalContent').innerHTML = forms[currentType](currentData);
// 关闭智能补充弹框
setTimeout(() => {
closeSmartUpdateModal();
}, 1500);
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>解析并补充';
}
// 监听编辑弹框打开,显示智能补充按钮
document.getElementById('editModal').addEventListener('showSmartUpdate', showSmartUpdateButton);
init();
</script>
</body>