feat: 发布日期、热度、置顶、图片上传功能
- 新增发布日期(publish_date)、热度(views)、置顶(is_pinned)字段 - 后台管理表格显示新字段和置顶操作按钮 - 前端默认排序:置顶优先 → 发布日期最新 - 新增多种排序选项:发布日期、热度、名称等 - 新增图片上传API(支持多图上传) - 后台管理表单添加图片上传组件(支持文件选择和粘贴) - 数据创建时自动初始化新字段
This commit is contained in:
331
app.py
331
app.py
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ParamHub - 参数百科
|
||||
AI大模型与硬件参数速查平台
|
||||
v1.1.0 - 新增智能添加和展示开关功能
|
||||
v1.4.0 - 新增图片上传功能
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, jsonify, request
|
||||
@@ -11,6 +11,10 @@ import requests
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import time
|
||||
import os
|
||||
import base64
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
||||
CORS(app)
|
||||
@@ -19,6 +23,16 @@ CORS(app)
|
||||
DATA_DIR = Path(__file__).parent / 'data'
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# 图片上传目录
|
||||
IMAGES_DIR = Path(__file__).parent / 'static' / 'uploads'
|
||||
IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 允许的图片格式
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
# 数据文件
|
||||
MODELS_FILE = DATA_DIR / 'models.json'
|
||||
GPUS_FILE = DATA_DIR / 'gpus.json'
|
||||
@@ -267,21 +281,51 @@ def api_models():
|
||||
keyword in m.get('organization', '').lower()]
|
||||
|
||||
# 排序
|
||||
sort_by = request.args.get('sort', 'created_at')
|
||||
reverse = request.args.get('order', 'asc') == 'desc'
|
||||
sort_by = request.args.get('sort', 'default')
|
||||
reverse = request.args.get('order', 'desc') == 'desc'
|
||||
|
||||
# 安全排序:处理可能的None/缺失值
|
||||
def safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
return 0 if key in ['parameters', 'context_length', 'mmlu'] else ''
|
||||
if key in ['parameters', 'context_length', 'mmlu', 'views']:
|
||||
return 0
|
||||
elif key in ['publish_date', 'created_at', 'updated_at']:
|
||||
return ''
|
||||
return ''
|
||||
return val
|
||||
|
||||
if sort_by in ['name', 'parameters', 'context_length', 'mmlu', 'created_at']:
|
||||
# 默认排序:置顶优先 → 发布日期最新
|
||||
if sort_by == 'default':
|
||||
# 先按置顶排序,再按发布日期排序
|
||||
def default_sort_key(x):
|
||||
is_pinned = x.get('is_pinned', False)
|
||||
publish_date = x.get('publish_date', '')
|
||||
created_at = x.get('created_at', '')
|
||||
# 置顶的排在前面 (True > False)
|
||||
# 发布日期按时间戳排序(越新的排在前面)
|
||||
return (not is_pinned, -(parse_date_to_timestamp(publish_date) or parse_date_to_timestamp(created_at) or 0))
|
||||
models = sorted(models, key=default_sort_key)
|
||||
elif sort_by in ['name', 'parameters', 'context_length', 'mmlu', 'created_at', 'publish_date', 'views', 'updated_at']:
|
||||
models = sorted(models, key=lambda x: safe_sort_key(x, sort_by), reverse=reverse)
|
||||
|
||||
return jsonify(models)
|
||||
|
||||
def parse_date_to_timestamp(date_str):
|
||||
"""将日期字符串转换为时间戳"""
|
||||
if not date_str:
|
||||
return None
|
||||
try:
|
||||
# 支持多种格式
|
||||
for fmt in ['%Y-%m-%d', '%Y-%m-%d %H:%M:%S', '%Y/%m/%d']:
|
||||
try:
|
||||
return datetime.strptime(date_str, fmt).timestamp()
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
@app.route('/api/models/<model_id>')
|
||||
def api_model_detail(model_id):
|
||||
"""获取单个模型详情"""
|
||||
@@ -302,6 +346,9 @@ def api_create_model():
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
data['visible'] = data.get('visible', True) # 默认显示
|
||||
data['publish_date'] = data.get('publish_date', '') # 发布日期
|
||||
data['views'] = data.get('views', 0) # 热度/阅读数
|
||||
data['is_pinned'] = data.get('is_pinned', False) # 置顶
|
||||
|
||||
models.append(data)
|
||||
save_data(MODELS_FILE, models)
|
||||
@@ -365,6 +412,9 @@ def api_smart_add_model():
|
||||
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)
|
||||
@@ -387,6 +437,9 @@ def api_smart_add_gpu():
|
||||
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
|
||||
|
||||
gpus = load_data(GPUS_FILE)
|
||||
gpus.append(parsed)
|
||||
@@ -408,6 +461,9 @@ def api_smart_add_cpu():
|
||||
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
|
||||
|
||||
cpus = load_data(CPUS_FILE)
|
||||
cpus.append(parsed)
|
||||
@@ -430,6 +486,9 @@ def api_smart_add_item(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
|
||||
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
items = load_data(items_file)
|
||||
@@ -455,6 +514,30 @@ def api_gpus():
|
||||
gpus = [g for g in gpus if keyword in g.get('name', '').lower() or
|
||||
keyword in g.get('manufacturer', '').lower()]
|
||||
|
||||
# 排序
|
||||
sort_by = request.args.get('sort', 'default')
|
||||
reverse = request.args.get('order', 'desc') == 'desc'
|
||||
|
||||
def safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
if key in ['memory_gb', 'cuda_cores', 'tensor_cores', 'views']:
|
||||
return 0
|
||||
elif key in ['publish_date', 'created_at', 'updated_at']:
|
||||
return ''
|
||||
return ''
|
||||
return val
|
||||
|
||||
if sort_by == 'default':
|
||||
def default_sort_key(x):
|
||||
is_pinned = x.get('is_pinned', False)
|
||||
publish_date = x.get('publish_date', '')
|
||||
created_at = x.get('created_at', '')
|
||||
return (not is_pinned, -(parse_date_to_timestamp(publish_date) or parse_date_to_timestamp(created_at) or 0))
|
||||
gpus = sorted(gpus, key=default_sort_key)
|
||||
elif sort_by in ['name', 'memory_gb', 'price_usd', 'created_at', 'publish_date', 'views', 'updated_at', 'release_year']:
|
||||
gpus = sorted(gpus, key=lambda x: safe_sort_key(x, sort_by), reverse=reverse)
|
||||
|
||||
return jsonify(gpus)
|
||||
|
||||
@app.route('/api/gpus/<gpu_id>')
|
||||
@@ -477,6 +560,9 @@ def api_create_gpu():
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
data['visible'] = data.get('visible', True)
|
||||
data['publish_date'] = data.get('publish_date', '')
|
||||
data['views'] = data.get('views', 0)
|
||||
data['is_pinned'] = data.get('is_pinned', False)
|
||||
|
||||
gpus.append(data)
|
||||
save_data(GPUS_FILE, gpus)
|
||||
@@ -538,6 +624,30 @@ def api_cpus():
|
||||
cpus = [c for c in cpus if keyword in c.get('name', '').lower() or
|
||||
keyword in c.get('manufacturer', '').lower()]
|
||||
|
||||
# 排序
|
||||
sort_by = request.args.get('sort', 'default')
|
||||
reverse = request.args.get('order', 'desc') == 'desc'
|
||||
|
||||
def safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
if key in ['cores', 'threads', 'views']:
|
||||
return 0
|
||||
elif key in ['publish_date', 'created_at', 'updated_at']:
|
||||
return ''
|
||||
return ''
|
||||
return val
|
||||
|
||||
if sort_by == 'default':
|
||||
def default_sort_key(x):
|
||||
is_pinned = x.get('is_pinned', False)
|
||||
publish_date = x.get('publish_date', '')
|
||||
created_at = x.get('created_at', '')
|
||||
return (not is_pinned, -(parse_date_to_timestamp(publish_date) or parse_date_to_timestamp(created_at) or 0))
|
||||
cpus = sorted(cpus, key=default_sort_key)
|
||||
elif sort_by in ['name', 'cores', 'threads', 'price_usd', 'created_at', 'publish_date', 'views', 'updated_at']:
|
||||
cpus = sorted(cpus, key=lambda x: safe_sort_key(x, sort_by), reverse=reverse)
|
||||
|
||||
return jsonify(cpus)
|
||||
|
||||
@app.route('/api/cpus/<cpu_id>')
|
||||
@@ -560,6 +670,9 @@ def api_create_cpu():
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
data['visible'] = data.get('visible', True)
|
||||
data['publish_date'] = data.get('publish_date', '')
|
||||
data['views'] = data.get('views', 0)
|
||||
data['is_pinned'] = data.get('is_pinned', False)
|
||||
|
||||
cpus.append(data)
|
||||
save_data(CPUS_FILE, cpus)
|
||||
@@ -832,6 +945,30 @@ def api_items(category_id):
|
||||
if hide_hidden:
|
||||
items = [i for i in items if i.get('visible', True)]
|
||||
|
||||
# 排序
|
||||
sort_by = request.args.get('sort', 'default')
|
||||
reverse = request.args.get('order', 'desc') == 'desc'
|
||||
|
||||
def safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
if key in ['price', 'views', 'year']:
|
||||
return 0
|
||||
elif key in ['publish_date', 'created_at', 'updated_at']:
|
||||
return ''
|
||||
return ''
|
||||
return val
|
||||
|
||||
if sort_by == 'default':
|
||||
def default_sort_key(x):
|
||||
is_pinned = x.get('is_pinned', False)
|
||||
publish_date = x.get('publish_date', '')
|
||||
created_at = x.get('created_at', '')
|
||||
return (not is_pinned, -(parse_date_to_timestamp(publish_date) or parse_date_to_timestamp(created_at) or 0))
|
||||
items = sorted(items, key=default_sort_key)
|
||||
elif sort_by in ['name', 'price', 'year', 'created_at', 'publish_date', 'views', 'updated_at']:
|
||||
items = sorted(items, key=lambda x: safe_sort_key(x, sort_by), reverse=reverse)
|
||||
|
||||
return jsonify(items)
|
||||
|
||||
@app.route('/api/items/<category_id>/<item_id>')
|
||||
@@ -857,6 +994,9 @@ def api_create_item(category_id):
|
||||
data['category_id'] = category_id
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
data['visible'] = data.get('visible', True)
|
||||
data['publish_date'] = data.get('publish_date', '')
|
||||
data['views'] = data.get('views', 0)
|
||||
data['is_pinned'] = data.get('is_pinned', False)
|
||||
|
||||
items.append(data)
|
||||
save_data(items_file, items)
|
||||
@@ -904,6 +1044,114 @@ def api_toggle_item_visible(category_id, item_id):
|
||||
|
||||
return jsonify({'success': True, 'visible': item['visible']})
|
||||
|
||||
# ============ 置顶和热度API ============
|
||||
|
||||
@app.route('/api/models/<model_id>/pin', methods=['POST'])
|
||||
def api_toggle_model_pin(model_id):
|
||||
"""切换模型置顶状态"""
|
||||
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
|
||||
|
||||
model['is_pinned'] = not model.get('is_pinned', False)
|
||||
save_data(MODELS_FILE, models)
|
||||
|
||||
return jsonify({'success': True, 'is_pinned': model['is_pinned']})
|
||||
|
||||
@app.route('/api/models/<model_id>/view', methods=['POST'])
|
||||
def api_model_view(model_id):
|
||||
"""增加模型阅读数"""
|
||||
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
|
||||
|
||||
model['views'] = model.get('views', 0) + 1
|
||||
save_data(MODELS_FILE, models)
|
||||
|
||||
return jsonify({'success': True, 'views': model['views']})
|
||||
|
||||
@app.route('/api/gpus/<gpu_id>/pin', methods=['POST'])
|
||||
def api_toggle_gpu_pin(gpu_id):
|
||||
"""切换GPU置顶状态"""
|
||||
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
|
||||
|
||||
gpu['is_pinned'] = not gpu.get('is_pinned', False)
|
||||
save_data(GPUS_FILE, gpus)
|
||||
|
||||
return jsonify({'success': True, 'is_pinned': gpu['is_pinned']})
|
||||
|
||||
@app.route('/api/gpus/<gpu_id>/view', methods=['POST'])
|
||||
def api_gpu_view(gpu_id):
|
||||
"""增加GPU阅读数"""
|
||||
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
|
||||
|
||||
gpu['views'] = gpu.get('views', 0) + 1
|
||||
save_data(GPUS_FILE, gpus)
|
||||
|
||||
return jsonify({'success': True, 'views': gpu['views']})
|
||||
|
||||
@app.route('/api/cpus/<cpu_id>/pin', methods=['POST'])
|
||||
def api_toggle_cpu_pin(cpu_id):
|
||||
"""切换CPU置顶状态"""
|
||||
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
|
||||
|
||||
cpu['is_pinned'] = not cpu.get('is_pinned', False)
|
||||
save_data(CPUS_FILE, cpus)
|
||||
|
||||
return jsonify({'success': True, 'is_pinned': cpu['is_pinned']})
|
||||
|
||||
@app.route('/api/cpus/<cpu_id>/view', methods=['POST'])
|
||||
def api_cpu_view(cpu_id):
|
||||
"""增加CPU阅读数"""
|
||||
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
|
||||
|
||||
cpu['views'] = cpu.get('views', 0) + 1
|
||||
save_data(CPUS_FILE, cpus)
|
||||
|
||||
return jsonify({'success': True, 'views': cpu['views']})
|
||||
|
||||
@app.route('/api/items/<category_id>/<item_id>/pin', methods=['POST'])
|
||||
def api_toggle_item_pin(category_id, item_id):
|
||||
"""切换动态数据置顶状态"""
|
||||
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
|
||||
|
||||
item['is_pinned'] = not item.get('is_pinned', False)
|
||||
save_data(items_file, items)
|
||||
|
||||
return jsonify({'success': True, 'is_pinned': item['is_pinned']})
|
||||
|
||||
@app.route('/api/items/<category_id>/<item_id>/view', methods=['POST'])
|
||||
def api_item_view(category_id, item_id):
|
||||
"""增加动态数据阅读数"""
|
||||
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
|
||||
|
||||
item['views'] = item.get('views', 0) + 1
|
||||
save_data(items_file, items)
|
||||
|
||||
return jsonify({'success': True, 'views': item['views']})
|
||||
|
||||
# ============ 网站配置API ============
|
||||
|
||||
@app.route('/api/config')
|
||||
@@ -921,9 +1169,80 @@ def api_update_config():
|
||||
save_config(config)
|
||||
return jsonify(config)
|
||||
|
||||
# ============ 图片上传API ============
|
||||
|
||||
@app.route('/api/upload/image', methods=['POST'])
|
||||
def api_upload_image():
|
||||
"""上传图片"""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': '没有文件'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': '没有选择文件'}), 400
|
||||
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({'error': '不允许的文件格式'}), 400
|
||||
|
||||
# 生成唯一文件名
|
||||
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||
filename = f"{uuid.uuid4().hex[:12]}_{int(time.time())}.{ext}"
|
||||
|
||||
# 保存文件
|
||||
filepath = IMAGES_DIR / filename
|
||||
file.save(filepath)
|
||||
|
||||
# 返回图片URL
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'filename': filename,
|
||||
'url': f'/static/uploads/{filename}'
|
||||
})
|
||||
|
||||
@app.route('/api/upload/image/base64', methods=['POST'])
|
||||
def api_upload_image_base64():
|
||||
"""上传Base64图片"""
|
||||
data = request.get_json()
|
||||
image_data = data.get('image', '')
|
||||
|
||||
if not image_data:
|
||||
return jsonify({'error': '没有图片数据'}), 400
|
||||
|
||||
# 解析Base64数据
|
||||
try:
|
||||
# 移除data:image/xxx;base64,前缀
|
||||
if 'base64,' in image_data:
|
||||
image_data = image_data.split('base64,')[1]
|
||||
|
||||
# 生成文件名
|
||||
ext = data.get('ext', 'png')
|
||||
filename = f"{uuid.uuid4().hex[:12]}_{int(time.time())}.{ext}"
|
||||
|
||||
# 保存文件
|
||||
filepath = IMAGES_DIR / filename
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(base64.b64decode(image_data))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'filename': filename,
|
||||
'url': f'/static/uploads/{filename}'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
@app.route('/api/upload/image/delete/<filename>', methods=['DELETE'])
|
||||
def api_delete_image(filename):
|
||||
"""删除图片"""
|
||||
filepath = IMAGES_DIR / filename
|
||||
if filepath.exists():
|
||||
filepath.unlink()
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("ParamHub - 参数百科 v1.2.0")
|
||||
print("ParamHub - 参数百科 v1.4.0")
|
||||
print("=" * 50)
|
||||
print(f"访问地址: http://localhost:19010")
|
||||
print(f"后台管理: http://localhost:19010/admin")
|
||||
|
||||
@@ -141,20 +141,25 @@
|
||||
<button onclick="openAddModal('model')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table class="w-full">
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
|
||||
<table class="w-full min-w-[1200px]">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<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-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-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-center text-sm font-medium text-gray-600">操作</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">参数量</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">上下文</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">类型</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-models-table"><tr><td colspan="7" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
<tbody id="admin-models-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -168,20 +173,25 @@
|
||||
<button onclick="openAddModal('gpu')" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table class="w-full">
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
|
||||
<table class="w-full min-w-[1200px]">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<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-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-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-center text-sm font-medium text-gray-600">操作</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">显存</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">架构</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">价格</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-gpus-table"><tr><td colspan="7" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
<tbody id="admin-gpus-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -195,20 +205,25 @@
|
||||
<button onclick="openAddModal('cpu')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table class="w-full">
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
|
||||
<table class="w-full min-w-[1200px]">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<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-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-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-center text-sm font-medium text-gray-600">操作</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">核心/线程</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">主频</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">价格</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
|
||||
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
|
||||
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="admin-cpus-table"><tr><td colspan="7" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
<tbody id="admin-cpus-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -351,6 +366,17 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
}
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
// 日期格式化(短格式)
|
||||
function formatDateShort(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
// 2026-04-20 18:30:00 -> 04-20 18:30
|
||||
const match = dateStr.match(/\d{4}-(\d{2}-\d{2})\s*(\d{2}:\d{2})?/);
|
||||
if (match) {
|
||||
return match[1] + (match[2] ? ' ' + match[2] : '');
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
@@ -623,21 +649,30 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
async function loadAdminModels() {
|
||||
const res = await fetch('/api/models?all=1');
|
||||
const models = await res.json();
|
||||
if (models.length === 0) { document.getElementById('admin-models-table').innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
if (models.length === 0) { document.getElementById('admin-models-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
document.getElementById('admin-models-table').innerHTML = models.map(m => `
|
||||
<tr class="border-b hover:bg-gray-50 ${m.visible === false ? 'bg-gray-100 opacity-60' : ''}">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">${m.name}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${m.organization}</td>
|
||||
<td class="px-4 py-3">${m.parameters}B</td>
|
||||
<td class="px-4 py-3 text-gray-600">${m.context_length || '-'}</td>
|
||||
<td class="px-4 py-3">${m.is_open_source ? '<span class="text-green-600">开源</span>' : '<span class="text-gray-600">商业</span>'}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<tr class="border-b hover:bg-gray-50 ${m.visible === false ? 'bg-gray-100 opacity-60' : ''} ${m.is_pinned ? 'bg-yellow-50' : ''}">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="togglePin('model', '${m.id}')" class="${m.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${m.is_pinned ? '取消置顶' : '置顶'}">
|
||||
<i class="${m.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-3 py-3 font-medium text-gray-800">${m.name}</td>
|
||||
<td class="px-3 py-3 text-gray-600">${m.organization}</td>
|
||||
<td class="px-3 py-3">${m.parameters}B</td>
|
||||
<td class="px-3 py-3 text-gray-600">${m.context_length || '-'}</td>
|
||||
<td class="px-3 py-3">${m.is_open_source ? '<span class="text-green-600">开源</span>' : '<span class="text-gray-600">商业</span>'}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${m.publish_date || '-'}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${m.views || 0}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(m.created_at)}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(m.updated_at)}</td>
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="toggleVisible('model', '${m.id}')" class="${m.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${m.visible === false ? '点击显示' : '点击隐藏'}">
|
||||
<i class="${m.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
|
||||
</button>
|
||||
${m.raw_text ? `<button onclick="showRawData('${m.id}', 'model')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="editItem('model', '${m.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
|
||||
<button onclick="deleteItem('model', '${m.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
|
||||
</td>
|
||||
@@ -649,21 +684,30 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
async function loadAdminGpus() {
|
||||
const res = await fetch('/api/gpus?all=1');
|
||||
const gpus = await res.json();
|
||||
if (gpus.length === 0) { document.getElementById('admin-gpus-table').innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
if (gpus.length === 0) { document.getElementById('admin-gpus-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
document.getElementById('admin-gpus-table').innerHTML = gpus.map(g => `
|
||||
<tr class="border-b hover:bg-gray-50 ${g.visible === false ? 'bg-gray-100 opacity-60' : ''}">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">${g.name}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${g.manufacturer}</td>
|
||||
<td class="px-4 py-3">${g.memory_gb}GB</td>
|
||||
<td class="px-4 py-3 text-gray-600">${g.architecture || '-'}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${formatPrice(g)}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<tr class="border-b hover:bg-gray-50 ${g.visible === false ? 'bg-gray-100 opacity-60' : ''} ${g.is_pinned ? 'bg-yellow-50' : ''}">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="togglePin('gpu', '${g.id}')" class="${g.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${g.is_pinned ? '取消置顶' : '置顶'}">
|
||||
<i class="${g.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-3 py-3 font-medium text-gray-800">${g.name}</td>
|
||||
<td class="px-3 py-3 text-gray-600">${g.manufacturer}</td>
|
||||
<td class="px-3 py-3">${g.memory_gb}GB</td>
|
||||
<td class="px-3 py-3 text-gray-600">${g.architecture || '-'}</td>
|
||||
<td class="px-3 py-3 text-gray-600">${formatPrice(g)}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${g.publish_date || '-'}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${g.views || 0}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(g.created_at)}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(g.updated_at)}</td>
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="toggleVisible('gpu', '${g.id}')" class="${g.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${g.visible === false ? '点击显示' : '点击隐藏'}">
|
||||
<i class="${g.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
|
||||
</button>
|
||||
${g.raw_text ? `<button onclick="showRawData('${g.id}', 'gpu')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="editItem('gpu', '${g.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
|
||||
<button onclick="deleteItem('gpu', '${g.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
|
||||
</td>
|
||||
@@ -675,21 +719,30 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
async function loadAdminCpus() {
|
||||
const res = await fetch('/api/cpus?all=1');
|
||||
const cpus = await res.json();
|
||||
if (cpus.length === 0) { document.getElementById('admin-cpus-table').innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
if (cpus.length === 0) { document.getElementById('admin-cpus-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
|
||||
document.getElementById('admin-cpus-table').innerHTML = cpus.map(c => `
|
||||
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''}">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">${c.name}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${c.manufacturer}</td>
|
||||
<td class="px-4 py-3">${c.cores}/${c.threads}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${c.base_clock_ghz || '-'}-${c.boost_clock_ghz || '-'}GHz</td>
|
||||
<td class="px-4 py-3 text-gray-600">${formatPrice(c)}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''} ${c.is_pinned ? 'bg-yellow-50' : ''}">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="togglePin('cpu', '${c.id}')" class="${c.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${c.is_pinned ? '取消置顶' : '置顶'}">
|
||||
<i class="${c.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-3 py-3 font-medium text-gray-800">${c.name}</td>
|
||||
<td class="px-3 py-3 text-gray-600">${c.manufacturer}</td>
|
||||
<td class="px-3 py-3">${c.cores}/${c.threads}</td>
|
||||
<td class="px-3 py-3 text-gray-600">${c.base_clock_ghz || '-'}-${c.boost_clock_ghz || '-'}GHz</td>
|
||||
<td class="px-3 py-3 text-gray-600">${formatPrice(c)}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${c.publish_date || '-'}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${c.views || 0}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(c.created_at)}</td>
|
||||
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(c.updated_at)}</td>
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="toggleVisible('cpu', '${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>
|
||||
${c.raw_text ? `<button onclick="showRawData('${c.id}', 'cpu')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<td class="px-3 py-3 text-center">
|
||||
<button onclick="editItem('cpu', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
|
||||
<button onclick="deleteItem('cpu', '${c.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
|
||||
</td>
|
||||
@@ -794,13 +847,22 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
const data = {};
|
||||
formData.forEach((value, key) => {
|
||||
if (value) {
|
||||
const numFields = ['parameters', 'context_length', 'mmlu', 'humaneval', 'input_price', 'output_price', 'memory_gb', 'cuda_cores', 'tensor_cores', 'memory_bandwidth_gbs', 'fp32_tflops', 'fp16_tflops', 'int8_perf_tops', 'price_usd', 'min_price', 'max_price', 'release_year', 'cores', 'threads', 'base_clock_ghz', 'boost_clock_ghz', 'l3_cache_mb', 'tdp_watts', 'order', 'price'];
|
||||
const numFields = ['parameters', 'context_length', 'mmlu', 'humaneval', 'input_price', 'output_price', 'memory_gb', 'cuda_cores', 'tensor_cores', 'memory_bandwidth_gbs', 'fp32_tflops', 'fp16_tflops', 'int8_perf_tops', 'price_usd', 'min_price', 'max_price', 'release_year', 'cores', 'threads', 'base_clock_ghz', 'boost_clock_ghz', 'l3_cache_mb', 'tdp_watts', 'order', 'price', 'views'];
|
||||
if (numFields.includes(key)) data[key] = parseFloat(value);
|
||||
else if (key === 'is_open_source' || key === 'visible') data[key] = value === 'true';
|
||||
else if (key === 'images') {
|
||||
try { data[key] = JSON.parse(value); } catch { data[key] = []; }
|
||||
}
|
||||
else data[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理图片数据
|
||||
const imagesInput = document.getElementById('imagesInput');
|
||||
if (imagesInput) {
|
||||
try { data['images'] = JSON.parse(imagesInput.value); } catch { data['images'] = []; }
|
||||
}
|
||||
|
||||
if (currentType === 'dynamic') {
|
||||
data.category_id = dynamicCategoryId;
|
||||
}
|
||||
@@ -830,6 +892,120 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
|
||||
function closeModal() { document.getElementById('editModal').classList.add('hidden'); }
|
||||
|
||||
// 图片上传组件
|
||||
function getImageUploadComponent(images = [], type) {
|
||||
const imageUrls = images || [];
|
||||
return `
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<label class="text-sm text-gray-600 mb-2 block">图片上传(支持多张)</label>
|
||||
<div class="flex flex-wrap gap-2 mb-2" id="imagePreviewArea">
|
||||
${imageUrls.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="removeImage(${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('')}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input type="file" id="imageInput" accept="image/*" multiple class="hidden" onchange="handleImageUpload(event, '${type}')">
|
||||
<button onclick="document.getElementById('imageInput').click()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
|
||||
<i class="ri-image-add-line mr-1"></i>选择图片
|
||||
</button>
|
||||
<button onclick="pasteImageFromClipboard('${type}')'" 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>
|
||||
</div>
|
||||
<input type="hidden" name="images" value="${JSON.stringify(imageUrls)}" id="imagesInput">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 当前图片列表
|
||||
let currentImages = [];
|
||||
|
||||
// 处理图片上传
|
||||
async function handleImageUpload(event, type) {
|
||||
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) {
|
||||
currentImages.push(data.url);
|
||||
updateImagePreview();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('上传失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
// 从剪贴板粘贴图片
|
||||
async function pasteImageFromClipboard(type) {
|
||||
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) {
|
||||
currentImages.push(data.url);
|
||||
updateImagePreview();
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
alert('无法从剪贴板获取图片,请使用文件选择');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除图片
|
||||
function removeImage(index) {
|
||||
currentImages.splice(index, 1);
|
||||
updateImagePreview();
|
||||
}
|
||||
|
||||
// 更新图片预览
|
||||
function updateImagePreview() {
|
||||
const area = document.getElementById('imagePreviewArea');
|
||||
if (!area) return;
|
||||
|
||||
area.innerHTML = currentImages.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="removeImage(${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('');
|
||||
|
||||
const input = document.getElementById('imagesInput');
|
||||
if (input) {
|
||||
input.value = JSON.stringify(currentImages);
|
||||
}
|
||||
}
|
||||
|
||||
// 表单模板
|
||||
function getCategoryForm(data = {}) {
|
||||
return `<form id="itemForm" class="space-y-4">
|
||||
@@ -870,19 +1046,24 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
|
||||
function getDynamicForm(data = {}) {
|
||||
const cat = categories.find(c => c.id === dynamicCategoryId);
|
||||
currentImages = data.images || [];
|
||||
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">品牌</label><input type="text" name="brand" value="${data.brand || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">价格</label><input type="number" name="price" value="${data.price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">年份</label><input type="number" name="year" value="${data.year || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
</div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">参数(JSON格式)</label><textarea name="specs" rows="4" class="w-full px-3 py-2 border rounded-lg font-mono text-sm" placeholder='{"key": "value"}'>${data.specs || ''}</textarea></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="2" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
|
||||
${getImageUploadComponent(currentImages, 'dynamic')}
|
||||
</form>`;
|
||||
}
|
||||
|
||||
function getModelForm(data = {}) {
|
||||
currentImages = data.images || [];
|
||||
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
@@ -896,12 +1077,16 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">输入价格($/1K)</label><input type="number" name="input_price" value="${data.input_price || ''}" step="0.001" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">输出价格($/1K)</label><input type="number" name="output_price" value="${data.output_price || ''}" step="0.001" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">许可证</label><input type="text" name="license" value="${data.license || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
</div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
|
||||
${getImageUploadComponent(currentImages, 'model')}
|
||||
</form>`;
|
||||
}
|
||||
|
||||
function getGpuForm(data = {}) {
|
||||
currentImages = data.images || [];
|
||||
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
@@ -921,13 +1106,16 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">最低价</label><input type="number" name="min_price" value="${data.min_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">最高价</label><input type="number" name="max_price" value="${data.max_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">价格单位</label><input type="text" name="price_unit" value="${data.price_unit || ''}" placeholder="如: 万" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">发布年份</label><input type="number" name="release_year" value="${data.release_year || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
</div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
|
||||
${getImageUploadComponent(currentImages, 'gpu')}
|
||||
</form>`;
|
||||
}
|
||||
|
||||
function getCpuForm(data = {}) {
|
||||
currentImages = data.images || [];
|
||||
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
@@ -948,8 +1136,11 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">最低价</label><input type="number" name="min_price" value="${data.min_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">最高价</label><input type="number" name="max_price" value="${data.max_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">价格单位</label><input type="text" name="price_unit" value="${data.price_unit || ''}" placeholder="如: 万" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
|
||||
</div>
|
||||
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
|
||||
${getImageUploadComponent(currentImages, 'cpu')}
|
||||
</form>`;
|
||||
}
|
||||
|
||||
@@ -1050,6 +1241,29 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 置顶切换功能 ============
|
||||
|
||||
async function togglePin(type, id) {
|
||||
let endpoint;
|
||||
if (type === 'model') endpoint = `/api/models/${id}/pin`;
|
||||
else if (type === 'gpu') endpoint = `/api/gpus/${id}/pin`;
|
||||
else if (type === 'cpu') endpoint = `/api/cpus/${id}/pin`;
|
||||
else if (type === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${id}/pin`;
|
||||
|
||||
try {
|
||||
await fetch(endpoint, { method: 'POST' });
|
||||
|
||||
// 刷新列表
|
||||
if (type === 'dynamic') showDynamicCategory(dynamicCategoryId);
|
||||
else {
|
||||
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
|
||||
loaders[type]();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('置顶切换失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 原始数据查看 ============
|
||||
|
||||
async function showRawData(id, type) {
|
||||
|
||||
@@ -51,9 +51,16 @@
|
||||
class="w-full pl-12 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:border-indigo-400"
|
||||
onkeyup="filterItems()">
|
||||
</div>
|
||||
<select id="sortSelect" onchange="sortItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
|
||||
<select id="sortBy" onchange="loadItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
|
||||
<option value="default">默认排序(置顶优先)</option>
|
||||
<option value="publish_date">按发布日期</option>
|
||||
<option value="views">按热度</option>
|
||||
<option value="name">按名称</option>
|
||||
<option value="created_at">按时间</option>
|
||||
<option value="created_at">按创建时间</option>
|
||||
</select>
|
||||
<select id="sortOrder" onchange="loadItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
|
||||
<option value="desc">降序</option>
|
||||
<option value="asc">升序</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +123,9 @@
|
||||
|
||||
// 加载数据
|
||||
async function loadItems() {
|
||||
const res = await fetch(`/api/items/${categoryId}`);
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
const sortOrder = document.getElementById('sortOrder').value;
|
||||
const res = await fetch(`/api/items/${categoryId}?sort=${sortBy}&order=${sortOrder}`);
|
||||
allItems = await res.json();
|
||||
|
||||
document.getElementById('itemCount').textContent = allItems.length;
|
||||
@@ -137,22 +146,28 @@
|
||||
|
||||
document.getElementById('itemsList').innerHTML = items.map(item => {
|
||||
const fields = Object.entries(item)
|
||||
.filter(([key, val]) => !['id', 'category_id', 'created_at', 'updated_at'].includes(key) && val)
|
||||
.filter(([key, val]) => !['id', 'category_id', 'created_at', 'updated_at', 'visible', 'is_pinned', 'views', 'publish_date'].includes(key) && val)
|
||||
.slice(0, 5)
|
||||
.map(([key, val]) => `<span class="text-gray-500 text-sm">${key}: ${val}</span>`)
|
||||
.join('<br>');
|
||||
|
||||
return `
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition group">
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition group ${item.is_pinned ? 'bg-yellow-50 border-yellow-300' : ''}">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-800 group-hover:text-indigo-600">${item.name || item.title || '未命名'}</h3>
|
||||
<h3 class="font-medium text-gray-800 group-hover:text-indigo-600 flex items-center gap-2">
|
||||
${item.is_pinned ? '<i class="ri-pushpin-fill text-yellow-500" title="置顶"></i>' : ''}
|
||||
${item.name || item.title || '未命名'}
|
||||
</h3>
|
||||
<div class="mt-2 space-y-1">
|
||||
${fields}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
${item.created_at ? item.created_at.split(' ')[0] : ''}
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-gray-400">
|
||||
${item.publish_date || (item.created_at ? item.created_at.split(' ')[0] : '')}
|
||||
</div>
|
||||
${item.views ? `<div class="text-xs text-gray-400 mt-1"><i class="ri-eye-line"></i> ${item.views}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,19 +191,6 @@
|
||||
renderItems(filtered);
|
||||
}
|
||||
|
||||
// 排序
|
||||
function sortItems() {
|
||||
const sortBy = document.getElementById('sortSelect').value;
|
||||
const sorted = [...allItems].sort((a, b) => {
|
||||
if (sortBy === 'name') {
|
||||
return (a.name || a.title || '').localeCompare(b.name || b.title || '');
|
||||
} else {
|
||||
return (b.created_at || '').localeCompare(a.created_at || '');
|
||||
}
|
||||
});
|
||||
renderItems(sorted);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadNav();
|
||||
loadItems();
|
||||
|
||||
@@ -42,14 +42,18 @@
|
||||
oninput="loadModels()">
|
||||
</div>
|
||||
<select id="sortBy" class="px-4 py-2 border border-gray-200 rounded-lg" onchange="loadModels()">
|
||||
<option value="default">默认排序(置顶优先)</option>
|
||||
<option value="publish_date">按发布日期</option>
|
||||
<option value="views">按热度</option>
|
||||
<option value="name">按名称</option>
|
||||
<option value="parameters">按参数量</option>
|
||||
<option value="mmlu">按MMLU分数</option>
|
||||
<option value="context_length">按上下文长度</option>
|
||||
<option value="created_at">按创建时间</option>
|
||||
</select>
|
||||
<select id="sortOrder" class="px-4 py-2 border border-gray-200 rounded-lg" onchange="loadModels()">
|
||||
<option value="asc">升序</option>
|
||||
<option value="desc">降序</option>
|
||||
<option value="asc">升序</option>
|
||||
</select>
|
||||
<select id="filterType" class="px-4 py-2 border border-gray-200 rounded-lg" onchange="loadModels()">
|
||||
<option value="all">全部</option>
|
||||
@@ -163,10 +167,15 @@
|
||||
}
|
||||
|
||||
const html = models.map(m => `
|
||||
<tr class="border-b hover:bg-gray-50 transition">
|
||||
<tr class="border-b hover:bg-gray-50 transition ${m.is_pinned ? 'bg-yellow-50' : ''}">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-medium text-gray-800">${m.name}</div>
|
||||
<div class="text-xs text-gray-500">${m.architecture || ''}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
${m.is_pinned ? '<i class="ri-pushpin-fill text-yellow-500" title="置顶"></i>' : ''}
|
||||
<div>
|
||||
<div class="font-medium text-gray-800">${m.name}</div>
|
||||
<div class="text-xs text-gray-500">${m.architecture || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600">${m.organization}</td>
|
||||
<td class="px-4 py-3">
|
||||
|
||||
Reference in New Issue
Block a user