feat: 发布日期、热度、置顶、图片上传功能

- 新增发布日期(publish_date)、热度(views)、置顶(is_pinned)字段
- 后台管理表格显示新字段和置顶操作按钮
- 前端默认排序:置顶优先 → 发布日期最新
- 新增多种排序选项:发布日期、热度、名称等
- 新增图片上传API(支持多图上传)
- 后台管理表单添加图片上传组件(支持文件选择和粘贴)
- 数据创建时自动初始化新字段
This commit is contained in:
2026-04-20 21:25:57 +08:00
parent 627148a87f
commit 45190980a9
4 changed files with 634 additions and 90 deletions

331
app.py
View File

@@ -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")

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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">