Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 867a0a3eaf | |||
| a647179e72 |
162
app.py
162
app.py
@@ -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():
|
||||
"""智能添加模型(支持文本和多图解析,可能添加多个产品)"""
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user