feat: 支持多图上传和智能解析产品参数
- 新增 /api/parse-images API 预览解析结果 - 智能添加支持多张图片上传和粘贴 - 支持一次解析出多个产品参数 - 所有类别(模型/GPU/CPU/动态分类)都支持图片解析 - 添加 vision_model 配置支持视觉模型
This commit is contained in:
299
app.py
299
app.py
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
ParamHub - 参数百科
|
ParamHub - 参数百科
|
||||||
AI大模型与硬件参数速查平台
|
AI大模型与硬件参数速查平台
|
||||||
v1.4.0 - 新增图片上传功能
|
v1.5.0 - 支持多图上传和智能解析产品参数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, render_template, jsonify, request
|
from flask import Flask, render_template, jsonify, request
|
||||||
@@ -46,6 +46,7 @@ LLM_CONFIG = {
|
|||||||
'base_url': 'http://192.168.2.17:19007/v1',
|
'base_url': 'http://192.168.2.17:19007/v1',
|
||||||
'api_key': 'xxxx',
|
'api_key': 'xxxx',
|
||||||
'model': 'auto',
|
'model': 'auto',
|
||||||
|
'vision_model': 'gpt-4-vision-preview', # 视觉模型(解析图片)
|
||||||
}
|
}
|
||||||
|
|
||||||
# 默认网站配置
|
# 默认网站配置
|
||||||
@@ -81,9 +82,10 @@ def save_data(file_path, data):
|
|||||||
|
|
||||||
# ============ 大模型智能解析 ============
|
# ============ 大模型智能解析 ============
|
||||||
|
|
||||||
def parse_with_llm(text, category_type):
|
def parse_with_llm(text, category_type, images=None):
|
||||||
"""
|
"""
|
||||||
使用大模型解析文本,提取结构化数据
|
使用大模型解析文本/图片,提取结构化数据
|
||||||
|
支持多张图片输入,可能解析出多个产品
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 根据类型定义字段模板
|
# 根据类型定义字段模板
|
||||||
@@ -139,7 +141,62 @@ def parse_with_llm(text, category_type):
|
|||||||
|
|
||||||
fields = field_templates.get(category_type, field_templates['dynamic'])
|
fields = field_templates.get(category_type, field_templates['dynamic'])
|
||||||
|
|
||||||
prompt = f"""请解析以下文本,提取结构化数据。
|
# 构建消息内容
|
||||||
|
content_parts = []
|
||||||
|
|
||||||
|
# 如果有图片,添加图片内容
|
||||||
|
if images and len(images) > 0:
|
||||||
|
content_parts.append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"""请分析图片中的产品参数信息,提取结构化数据。
|
||||||
|
|
||||||
|
需要提取的字段:
|
||||||
|
{json.dumps(fields, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
重要要求:
|
||||||
|
1. 图片中可能包含1个或多个产品,请识别所有产品
|
||||||
|
2. 如果是多张图片,请综合分析所有图片内容
|
||||||
|
3. 数字字段只返回数字,不带单位
|
||||||
|
4. 如果某字段没有提及,返回null
|
||||||
|
5. 返回格式:如果识别到多个产品,返回数组 [{"name": ...}, {"name": ...}]; 如果只有一个产品,返回单个对象 {"name": ...}
|
||||||
|
6. 只返回JSON数据,不要其他内容"""
|
||||||
|
})
|
||||||
|
|
||||||
|
# 添加每张图片(支持URL或base64)
|
||||||
|
for img in images:
|
||||||
|
if isinstance(img, str):
|
||||||
|
if img.startswith('http'):
|
||||||
|
# URL图片
|
||||||
|
content_parts.append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": img}
|
||||||
|
})
|
||||||
|
elif img.startswith('data:'):
|
||||||
|
# base64图片
|
||||||
|
content_parts.append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": img}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 本地路径,读取并转为base64
|
||||||
|
try:
|
||||||
|
img_path = IMAGES_DIR / img.replace('/static/uploads/', '')
|
||||||
|
if img_path.exists():
|
||||||
|
with open(img_path, 'rb') as f:
|
||||||
|
img_data = base64.b64encode(f.read()).decode()
|
||||||
|
ext = img_path.suffix.lower()
|
||||||
|
mime_type = f'image/{ext if ext != "jpg" else "jpeg"}'
|
||||||
|
content_parts.append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": f"data:{mime_type};base64,{img_data}"}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取图片失败: {e}")
|
||||||
|
else:
|
||||||
|
# 纯文本解析
|
||||||
|
content_parts.append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"""请解析以下文本,提取结构化数据。
|
||||||
|
|
||||||
文本内容:
|
文本内容:
|
||||||
{text}
|
{text}
|
||||||
@@ -154,8 +211,12 @@ def parse_with_llm(text, category_type):
|
|||||||
4. 返回JSON格式,不要包含任何其他内容
|
4. 返回JSON格式,不要包含任何其他内容
|
||||||
|
|
||||||
请直接返回JSON数据:"""
|
请直接返回JSON数据:"""
|
||||||
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 使用视觉模型解析
|
||||||
|
model = LLM_CONFIG.get('vision_model', 'gpt-4-vision-preview') if images else LLM_CONFIG['model']
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{LLM_CONFIG['base_url']}/chat/completions",
|
f"{LLM_CONFIG['base_url']}/chat/completions",
|
||||||
headers={
|
headers={
|
||||||
@@ -163,15 +224,15 @@ def parse_with_llm(text, category_type):
|
|||||||
"Authorization": f"Bearer {LLM_CONFIG['api_key']}"
|
"Authorization": f"Bearer {LLM_CONFIG['api_key']}"
|
||||||
},
|
},
|
||||||
json={
|
json={
|
||||||
"model": LLM_CONFIG['model'],
|
"model": model,
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "你是一个数据提取助手,负责从文本中提取结构化数据。只返回JSON,不要其他内容。"},
|
{"role": "system", "content": "你是一个产品参数提取助手,负责从文本和图片中提取结构化的产品参数数据。只返回JSON,不要其他内容。如果图片中包含多个产品,返回数组。"},
|
||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": content_parts}
|
||||||
],
|
],
|
||||||
"max_tokens": 1000,
|
"max_tokens": 2000,
|
||||||
"temperature": 0.1
|
"temperature": 0.1
|
||||||
},
|
},
|
||||||
timeout=30
|
timeout=60
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -186,28 +247,38 @@ def parse_with_llm(text, category_type):
|
|||||||
# 解析JSON
|
# 解析JSON
|
||||||
parsed = json.loads(content)
|
parsed = json.loads(content)
|
||||||
|
|
||||||
# 清理null值
|
# 处理结果(可能是数组或单个对象)
|
||||||
cleaned = {}
|
results = []
|
||||||
for k, v in parsed.items():
|
if isinstance(parsed, list):
|
||||||
if v is not None and v != '' and v != 'null':
|
results = parsed
|
||||||
# 尝试转换数字
|
else:
|
||||||
if isinstance(v, str):
|
results = [parsed]
|
||||||
try:
|
|
||||||
if '.' in v:
|
|
||||||
cleaned[k] = float(v)
|
|
||||||
else:
|
|
||||||
cleaned[k] = int(v)
|
|
||||||
except:
|
|
||||||
cleaned[k] = v
|
|
||||||
else:
|
|
||||||
cleaned[k] = v
|
|
||||||
|
|
||||||
return cleaned
|
# 清理每个结果的null值
|
||||||
|
cleaned_results = []
|
||||||
|
for item in results:
|
||||||
|
cleaned = {}
|
||||||
|
for k, v in item.items():
|
||||||
|
if v is not None and v != '' and v != 'null':
|
||||||
|
# 尝试转换数字
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
if '.' in v:
|
||||||
|
cleaned[k] = float(v)
|
||||||
|
else:
|
||||||
|
cleaned[k] = int(v)
|
||||||
|
except:
|
||||||
|
cleaned[k] = v
|
||||||
|
else:
|
||||||
|
cleaned[k] = v
|
||||||
|
cleaned_results.append(cleaned)
|
||||||
|
|
||||||
|
return cleaned_results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"LLM解析失败: {e}")
|
print(f"LLM解析失败: {e}")
|
||||||
|
|
||||||
# 降级处理:返回基本结构
|
# 降级处理:返回基本结构
|
||||||
return {'name': text[:50], 'description': text}
|
return [{'name': text[:50] if text else '未命名产品', 'description': text}]
|
||||||
|
|
||||||
# ============ 页面路由 ============
|
# ============ 页面路由 ============
|
||||||
|
|
||||||
@@ -393,109 +464,171 @@ def api_toggle_model_visible(model_id):
|
|||||||
|
|
||||||
return jsonify({'success': True, 'visible': model['visible']})
|
return jsonify({'success': True, 'visible': model['visible']})
|
||||||
|
|
||||||
|
# ============ 图片解析API(预览) ============
|
||||||
|
|
||||||
|
@app.route('/api/parse-images', methods=['POST'])
|
||||||
|
def api_parse_images():
|
||||||
|
"""
|
||||||
|
解析图片中的产品参数(预览模式,不保存)
|
||||||
|
支持多张图片,可能返回多个产品
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text', '')
|
||||||
|
images = data.get('images', [])
|
||||||
|
category_type = data.get('category_type', 'dynamic')
|
||||||
|
|
||||||
|
if not text and not images:
|
||||||
|
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
return jsonify({'error': '请上传至少一张图片'}), 400
|
||||||
|
|
||||||
|
# 调用大模型解析
|
||||||
|
parsed_list = parse_with_llm(text, category_type, images)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'count': len(parsed_list),
|
||||||
|
'products': parsed_list,
|
||||||
|
'raw_text': text,
|
||||||
|
'images': images
|
||||||
|
})
|
||||||
|
|
||||||
# ============ 智能添加API ============
|
# ============ 智能添加API ============
|
||||||
|
|
||||||
@app.route('/api/models/smart-add', methods=['POST'])
|
@app.route('/api/models/smart-add', methods=['POST'])
|
||||||
def api_smart_add_model():
|
def api_smart_add_model():
|
||||||
"""智能添加模型(粘贴文本解析)"""
|
"""智能添加模型(支持文本和多图解析,可能添加多个产品)"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
text = data.get('text', '')
|
text = data.get('text', '')
|
||||||
|
images = data.get('images', [])
|
||||||
|
|
||||||
if not text:
|
if not text and not images:
|
||||||
return jsonify({'error': '文本不能为空'}), 400
|
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||||
|
|
||||||
# 大模型解析
|
# 大模型解析(支持多图)
|
||||||
parsed = parse_with_llm(text, 'model')
|
parsed_list = parse_with_llm(text, 'model', images)
|
||||||
|
|
||||||
# 补充必要字段
|
# 处理多个产品
|
||||||
parsed['id'] = uuid.uuid4().hex[:12]
|
results = []
|
||||||
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
parsed['visible'] = True
|
|
||||||
parsed['raw_text'] = text # 保存原始文本
|
|
||||||
parsed['publish_date'] = parsed.get('publish_date', '') # 发布日期
|
|
||||||
parsed['views'] = 0 # 热度初始化为0
|
|
||||||
parsed['is_pinned'] = False # 置顶初始化为False
|
|
||||||
|
|
||||||
# 保存
|
|
||||||
models = load_data(MODELS_FILE)
|
models = load_data(MODELS_FILE)
|
||||||
models.append(parsed)
|
|
||||||
|
for parsed in parsed_list:
|
||||||
|
# 补充必要字段
|
||||||
|
parsed['id'] = uuid.uuid4().hex[:12]
|
||||||
|
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
parsed['visible'] = True
|
||||||
|
parsed['raw_text'] = text # 保存原始文本
|
||||||
|
parsed['images'] = images # 保存图片
|
||||||
|
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||||
|
parsed['views'] = 0
|
||||||
|
parsed['is_pinned'] = False
|
||||||
|
|
||||||
|
models.append(parsed)
|
||||||
|
results.append(parsed)
|
||||||
|
|
||||||
save_data(MODELS_FILE, models)
|
save_data(MODELS_FILE, models)
|
||||||
|
|
||||||
return jsonify(parsed)
|
# 返回添加的产品列表
|
||||||
|
return jsonify({'success': True, 'count': len(results), 'products': results})
|
||||||
|
|
||||||
@app.route('/api/gpus/smart-add', methods=['POST'])
|
@app.route('/api/gpus/smart-add', methods=['POST'])
|
||||||
def api_smart_add_gpu():
|
def api_smart_add_gpu():
|
||||||
"""智能添加GPU"""
|
"""智能添加GPU(支持文本和多图解析)"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
text = data.get('text', '')
|
text = data.get('text', '')
|
||||||
|
images = data.get('images', [])
|
||||||
|
|
||||||
if not text:
|
if not text and not images:
|
||||||
return jsonify({'error': '文本不能为空'}), 400
|
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||||
|
|
||||||
parsed = parse_with_llm(text, 'gpu')
|
parsed_list = parse_with_llm(text, 'gpu', images)
|
||||||
parsed['id'] = uuid.uuid4().hex[:12]
|
|
||||||
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
parsed['visible'] = True
|
|
||||||
parsed['raw_text'] = text
|
|
||||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
|
||||||
parsed['views'] = 0
|
|
||||||
parsed['is_pinned'] = False
|
|
||||||
|
|
||||||
|
results = []
|
||||||
gpus = load_data(GPUS_FILE)
|
gpus = load_data(GPUS_FILE)
|
||||||
gpus.append(parsed)
|
|
||||||
|
for parsed in parsed_list:
|
||||||
|
parsed['id'] = uuid.uuid4().hex[:12]
|
||||||
|
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
parsed['visible'] = True
|
||||||
|
parsed['raw_text'] = text
|
||||||
|
parsed['images'] = images
|
||||||
|
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||||
|
parsed['views'] = 0
|
||||||
|
parsed['is_pinned'] = False
|
||||||
|
|
||||||
|
gpus.append(parsed)
|
||||||
|
results.append(parsed)
|
||||||
|
|
||||||
save_data(GPUS_FILE, gpus)
|
save_data(GPUS_FILE, gpus)
|
||||||
|
|
||||||
return jsonify(parsed)
|
return jsonify({'success': True, 'count': len(results), 'products': results})
|
||||||
|
|
||||||
@app.route('/api/cpus/smart-add', methods=['POST'])
|
@app.route('/api/cpus/smart-add', methods=['POST'])
|
||||||
def api_smart_add_cpu():
|
def api_smart_add_cpu():
|
||||||
"""智能添加CPU"""
|
"""智能添加CPU(支持文本和多图解析)"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
text = data.get('text', '')
|
text = data.get('text', '')
|
||||||
|
images = data.get('images', [])
|
||||||
|
|
||||||
if not text:
|
if not text and not images:
|
||||||
return jsonify({'error': '文本不能为空'}), 400
|
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||||
|
|
||||||
parsed = parse_with_llm(text, 'cpu')
|
parsed_list = parse_with_llm(text, 'cpu', images)
|
||||||
parsed['id'] = uuid.uuid4().hex[:12]
|
|
||||||
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
parsed['visible'] = True
|
|
||||||
parsed['raw_text'] = text
|
|
||||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
|
||||||
parsed['views'] = 0
|
|
||||||
parsed['is_pinned'] = False
|
|
||||||
|
|
||||||
|
results = []
|
||||||
cpus = load_data(CPUS_FILE)
|
cpus = load_data(CPUS_FILE)
|
||||||
cpus.append(parsed)
|
|
||||||
|
for parsed in parsed_list:
|
||||||
|
parsed['id'] = uuid.uuid4().hex[:12]
|
||||||
|
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
parsed['visible'] = True
|
||||||
|
parsed['raw_text'] = text
|
||||||
|
parsed['images'] = images
|
||||||
|
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||||
|
parsed['views'] = 0
|
||||||
|
parsed['is_pinned'] = False
|
||||||
|
|
||||||
|
cpus.append(parsed)
|
||||||
|
results.append(parsed)
|
||||||
|
|
||||||
save_data(CPUS_FILE, cpus)
|
save_data(CPUS_FILE, cpus)
|
||||||
|
|
||||||
return jsonify(parsed)
|
return jsonify({'success': True, 'count': len(results), 'products': results})
|
||||||
|
|
||||||
@app.route('/api/items/<category_id>/smart-add', methods=['POST'])
|
@app.route('/api/items/<category_id>/smart-add', methods=['POST'])
|
||||||
def api_smart_add_item(category_id):
|
def api_smart_add_item(category_id):
|
||||||
"""智能添加动态分类数据"""
|
"""智能添加动态分类数据(支持文本和多图解析)"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
text = data.get('text', '')
|
text = data.get('text', '')
|
||||||
|
images = data.get('images', [])
|
||||||
|
|
||||||
if not text:
|
if not text and not images:
|
||||||
return jsonify({'error': '文本不能为空'}), 400
|
return jsonify({'error': '文本或图片不能都为空'}), 400
|
||||||
|
|
||||||
parsed = parse_with_llm(text, 'dynamic')
|
parsed_list = parse_with_llm(text, 'dynamic', images)
|
||||||
parsed['id'] = uuid.uuid4().hex[:12]
|
|
||||||
parsed['category_id'] = category_id
|
|
||||||
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
parsed['visible'] = True
|
|
||||||
parsed['raw_text'] = text
|
|
||||||
parsed['publish_date'] = parsed.get('publish_date', '')
|
|
||||||
parsed['views'] = 0
|
|
||||||
parsed['is_pinned'] = False
|
|
||||||
|
|
||||||
|
results = []
|
||||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||||
items = load_data(items_file)
|
items = load_data(items_file)
|
||||||
items.append(parsed)
|
|
||||||
|
for parsed in parsed_list:
|
||||||
|
parsed['id'] = uuid.uuid4().hex[:12]
|
||||||
|
parsed['category_id'] = category_id
|
||||||
|
parsed['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
parsed['visible'] = True
|
||||||
|
parsed['raw_text'] = text
|
||||||
|
parsed['images'] = images
|
||||||
|
parsed['publish_date'] = parsed.get('publish_date', '')
|
||||||
|
parsed['views'] = 0
|
||||||
|
parsed['is_pinned'] = False
|
||||||
|
|
||||||
|
items.append(parsed)
|
||||||
|
results.append(parsed)
|
||||||
|
|
||||||
save_data(items_file, items)
|
save_data(items_file, items)
|
||||||
|
|
||||||
return jsonify(parsed)
|
return jsonify({'success': True, 'count': len(results), 'products': results})
|
||||||
|
|
||||||
# ============ GPU API ============
|
# ============ GPU API ============
|
||||||
|
|
||||||
|
|||||||
@@ -277,25 +277,52 @@
|
|||||||
|
|
||||||
<!-- 智能添加弹窗 -->
|
<!-- 智能添加弹窗 -->
|
||||||
<div id="smartAddModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
<div id="smartAddModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||||
<div class="bg-white rounded-xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-auto">
|
<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">
|
<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>
|
<h2 class="text-xl font-bold text-gray-800"><i class="ri-magic-line mr-2 text-orange-600"></i>智能添加(支持多图解析)</h2>
|
||||||
<button onclick="closeSmartAddModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
|
<button onclick="closeSmartAddModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<p class="text-sm text-gray-500 mb-4">粘贴产品信息文本,AI将自动解析并提取结构化数据。支持各种格式的产品介绍、规格参数、价格信息等。</p>
|
<div class="mb-6">
|
||||||
<textarea id="smartAddText" rows="8" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="粘贴产品信息文本...
|
<p class="text-sm text-gray-500 mb-3">上传产品图片,AI将自动识别并解析参数。支持一次上传多张图片,可识别多个产品。</p>
|
||||||
|
<div class="flex flex-wrap gap-3 mb-3" id="smartImagePreviewArea">
|
||||||
示例:
|
<!-- 图片预览区 -->
|
||||||
GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上下文,MMLU分数86.4,输入价格$0.03/1K tokens,输出价格$0.06/1K tokens,商业许可。"></textarea>
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<input type="file" id="smartImageInput" accept="image/*" multiple class="hidden" onchange="handleSmartImageUpload(event)">
|
||||||
|
<button onclick="document.getElementById('smartImageInput').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="pasteSmartImageFromClipboard()" 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="clearSmartImages()" 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="smartImageCount">0</span> 张图片
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-t pt-4">
|
||||||
|
<label class="text-sm text-gray-600 mb-2 block">补充文本(可选)</label>
|
||||||
|
<textarea id="smartAddText" 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="smartAddPreview" class="mt-4 hidden">
|
<div id="smartAddPreview" class="mt-4 hidden">
|
||||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">解析结果预览:</h3>
|
<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="smartAddResult"></div>
|
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartAddResult">
|
||||||
|
<!-- 解析结果显示 -->
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex gap-2" id="smartAddActions">
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
|
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
|
||||||
<button onclick="closeSmartAddModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
|
<button onclick="closeSmartAddModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
|
||||||
<button onclick="smartAddSubmit()" id="smartAddBtn" 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>
|
<button onclick="previewSmartParse()" id="previewBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-eye-line mr-1"></i>预览解析结果</button>
|
||||||
|
<button onclick="smartAddSubmit()" id="smartAddBtn" 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1151,11 +1178,15 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
|||||||
// ============ 智能添加功能 ============
|
// ============ 智能添加功能 ============
|
||||||
|
|
||||||
let smartAddType = '';
|
let smartAddType = '';
|
||||||
|
let smartAddImages = []; // 智能添加的图片列表
|
||||||
|
|
||||||
function openSmartAddModal(type) {
|
function openSmartAddModal(type) {
|
||||||
smartAddType = type;
|
smartAddType = type;
|
||||||
|
smartAddImages = [];
|
||||||
document.getElementById('smartAddText').value = '';
|
document.getElementById('smartAddText').value = '';
|
||||||
document.getElementById('smartAddPreview').classList.add('hidden');
|
document.getElementById('smartAddPreview').classList.add('hidden');
|
||||||
|
document.getElementById('smartImagePreviewArea').innerHTML = '';
|
||||||
|
document.getElementById('smartImageCount').textContent = '0';
|
||||||
document.getElementById('smartAddModal').classList.remove('hidden');
|
document.getElementById('smartAddModal').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,16 +1194,158 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
|||||||
document.getElementById('smartAddModal').classList.add('hidden');
|
document.getElementById('smartAddModal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理智能添加图片上传
|
||||||
|
async function handleSmartImageUpload(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) {
|
||||||
|
smartAddImages.push(data.url);
|
||||||
|
updateSmartImagePreview();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('上传失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从剪贴板粘贴图片
|
||||||
|
async function pasteSmartImageFromClipboard() {
|
||||||
|
try {
|
||||||
|
const clipboardItems = await navigator.clipboard.read();
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
for (const type of item.types) {
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
const blob = await item.getType(type);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const base64 = e.target.result;
|
||||||
|
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) {
|
||||||
|
smartAddImages.push(data.url);
|
||||||
|
updateSmartImagePreview();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('无法从剪贴板获取图片,请使用文件选择');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空智能添加图片
|
||||||
|
function clearSmartImages() {
|
||||||
|
smartAddImages = [];
|
||||||
|
updateSmartImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除单张图片
|
||||||
|
function removeSmartImage(index) {
|
||||||
|
smartAddImages.splice(index, 1);
|
||||||
|
updateSmartImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新智能添加图片预览
|
||||||
|
function updateSmartImagePreview() {
|
||||||
|
const area = document.getElementById('smartImagePreviewArea');
|
||||||
|
const count = document.getElementById('smartImageCount');
|
||||||
|
|
||||||
|
area.innerHTML = smartAddImages.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="removeSmartImage(${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 = smartAddImages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览解析结果
|
||||||
|
async function previewSmartParse() {
|
||||||
|
const text = document.getElementById('smartAddText').value.trim();
|
||||||
|
|
||||||
|
if (!text && smartAddImages.length === 0) {
|
||||||
|
alert('请上传图片或输入文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('previewBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/parse-images', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
images: smartAddImages,
|
||||||
|
category_type: smartAddType
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert('解析失败: ' + data.error);
|
||||||
|
} else {
|
||||||
|
// 显示解析结果
|
||||||
|
document.getElementById('smartAddPreview').classList.remove('hidden');
|
||||||
|
|
||||||
|
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-line mr-1"></i>识别到 ${data.count} 个产品</div>`;
|
||||||
|
|
||||||
|
data.products.forEach((product, idx) => {
|
||||||
|
html += `
|
||||||
|
<div class="bg-white rounded-lg p-3 mb-2 border">
|
||||||
|
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
|
||||||
|
<div class="text-xs text-gray-600 grid grid-cols-2 gap-2">
|
||||||
|
${Object.entries(product).map(([k, v]) => `
|
||||||
|
<div><span class="text-gray-400">${k}:</span> ${v || '-'}</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('smartAddResult').innerHTML = html;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('请求失败: ' + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="ri-eye-line mr-1"></i>预览解析结果';
|
||||||
|
}
|
||||||
|
|
||||||
async function smartAddSubmit() {
|
async function smartAddSubmit() {
|
||||||
const text = document.getElementById('smartAddText').value.trim();
|
const text = document.getElementById('smartAddText').value.trim();
|
||||||
if (!text) {
|
|
||||||
alert('请粘贴产品信息文本');
|
if (!text && smartAddImages.length === 0) {
|
||||||
|
alert('请上传图片或输入文本');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('smartAddBtn');
|
const btn = document.getElementById('smartAddBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
|
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析并保存中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let endpoint;
|
let endpoint;
|
||||||
@@ -1184,7 +1357,10 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
|||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text })
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
images: smartAddImages
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -1194,26 +1370,38 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
|||||||
} else {
|
} else {
|
||||||
// 显示解析结果
|
// 显示解析结果
|
||||||
document.getElementById('smartAddPreview').classList.remove('hidden');
|
document.getElementById('smartAddPreview').classList.remove('hidden');
|
||||||
document.getElementById('smartAddResult').innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
||||||
|
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-fill mr-1"></i>成功添加 ${data.count} 个产品</div>`;
|
||||||
|
|
||||||
|
data.products.forEach((product, idx) => {
|
||||||
|
html += `
|
||||||
|
<div class="bg-white rounded-lg p-3 mb-2 border border-green-200">
|
||||||
|
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
|
||||||
|
<div class="text-xs text-gray-500">ID: ${product.id}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('smartAddResult').innerHTML = html;
|
||||||
|
|
||||||
// 关闭弹窗并刷新列表
|
// 关闭弹窗并刷新列表
|
||||||
closeSmartAddModal();
|
setTimeout(() => {
|
||||||
|
closeSmartAddModal();
|
||||||
|
|
||||||
if (smartAddType === 'dynamic') showDynamicCategory(dynamicCategoryId);
|
if (smartAddType === 'dynamic') showDynamicCategory(dynamicCategoryId);
|
||||||
else {
|
else {
|
||||||
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
|
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
|
||||||
loaders[smartAddType]();
|
loaders[smartAddType]();
|
||||||
}
|
}
|
||||||
loadOverview();
|
loadOverview();
|
||||||
|
}, 1500);
|
||||||
alert('智能添加成功!数据已自动解析并保存。');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('请求失败: ' + e.message);
|
alert('请求失败: ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>智能解析并添加';
|
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>解析并添加';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 显示切换功能 ============
|
// ============ 显示切换功能 ============
|
||||||
|
|||||||
Reference in New Issue
Block a user