diff --git a/app.py b/app.py index 1cd12a2..62048fb 100644 --- a/app.py +++ b/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/') 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/') @@ -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/') @@ -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//') @@ -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//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//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//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//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//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//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///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///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/', 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") diff --git a/templates/admin.html b/templates/admin.html index c83fae7..82aa9d2 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -141,20 +141,25 @@ -
- +
+
- - - - - - - + + + + + + + + + + + + - +
名称厂商参数量上下文类型显示操作置顶名称厂商参数量上下文类型发布日期热度创建时间更新时间显示操作
加载中...
加载中...
@@ -168,20 +173,25 @@ -
- +
+
- - - - - - - + + + + + + + + + + + + - +
名称厂商显存架构价格显示操作置顶名称厂商显存架构价格发布日期热度创建时间更新时间显示操作
加载中...
加载中...
@@ -195,20 +205,25 @@ -
- +
+
- - - - - - - + + + + + + + + + + + + - +
名称厂商核心/线程主频价格显示操作置顶名称厂商核心/线程主频价格发布日期热度创建时间更新时间显示操作
加载中...
加载中...
@@ -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 = '暂无数据'; return; } + if (models.length === 0) { document.getElementById('admin-models-table').innerHTML = '暂无数据'; return; } document.getElementById('admin-models-table').innerHTML = models.map(m => ` - - ${m.name} - ${m.organization} - ${m.parameters}B - ${m.context_length || '-'} - ${m.is_open_source ? '开源' : '商业'} - + + + + + ${m.name} + ${m.organization} + ${m.parameters}B + ${m.context_length || '-'} + ${m.is_open_source ? '开源' : '商业'} + ${m.publish_date || '-'} + ${m.views || 0} + ${formatDateShort(m.created_at)} + ${formatDateShort(m.updated_at)} + ${m.raw_text ? `` : ''} - + @@ -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 = '暂无数据'; return; } + if (gpus.length === 0) { document.getElementById('admin-gpus-table').innerHTML = '暂无数据'; return; } document.getElementById('admin-gpus-table').innerHTML = gpus.map(g => ` - - ${g.name} - ${g.manufacturer} - ${g.memory_gb}GB - ${g.architecture || '-'} - ${formatPrice(g)} - + + + + + ${g.name} + ${g.manufacturer} + ${g.memory_gb}GB + ${g.architecture || '-'} + ${formatPrice(g)} + ${g.publish_date || '-'} + ${g.views || 0} + ${formatDateShort(g.created_at)} + ${formatDateShort(g.updated_at)} + ${g.raw_text ? `` : ''} - + @@ -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 = '暂无数据'; return; } + if (cpus.length === 0) { document.getElementById('admin-cpus-table').innerHTML = '暂无数据'; return; } document.getElementById('admin-cpus-table').innerHTML = cpus.map(c => ` - - ${c.name} - ${c.manufacturer} - ${c.cores}/${c.threads} - ${c.base_clock_ghz || '-'}-${c.boost_clock_ghz || '-'}GHz - ${formatPrice(c)} - + + + + + ${c.name} + ${c.manufacturer} + ${c.cores}/${c.threads} + ${c.base_clock_ghz || '-'}-${c.boost_clock_ghz || '-'}GHz + ${formatPrice(c)} + ${c.publish_date || '-'} + ${c.views || 0} + ${formatDateShort(c.created_at)} + ${formatDateShort(c.updated_at)} + ${c.raw_text ? `` : ''} - + @@ -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 ` +
+ +
+ ${imageUrls.map((url, idx) => ` +
+ + +
+ `).join('')} +
+
+ + + +
+ +
+ `; + } + + // 当前图片列表 + 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) => ` +
+ + +
+ `).join(''); + + const input = document.getElementById('imagesInput'); + if (input) { + input.value = JSON.stringify(currentImages); + } + } + // 表单模板 function getCategoryForm(data = {}) { return `
@@ -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 `
+
+
+ ${getImageUploadComponent(currentImages, 'dynamic')}
`; } function getModelForm(data = {}) { + currentImages = data.images || []; return `
@@ -896,12 +1077,16 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
+
+
+ ${getImageUploadComponent(currentImages, 'model')}
`; } function getGpuForm(data = {}) { + currentImages = data.images || []; return `
@@ -921,13 +1106,16 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
-
+
+
+ ${getImageUploadComponent(currentImages, 'gpu')}
`; } function getCpuForm(data = {}) { + currentImages = data.images || []; return `
@@ -948,8 +1136,11 @@ GPT-4是OpenAI发布的大语言模型,参数量约1.8万亿,支持128K上
+
+
+ ${getImageUploadComponent(currentImages, 'cpu')}
`; } @@ -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) { diff --git a/templates/category.html b/templates/category.html index 2eae90c..15926ee 100644 --- a/templates/category.html +++ b/templates/category.html @@ -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()"> - + + + - + + + @@ -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]) => `${key}: ${val}`) .join('
'); return ` -
+
-

${item.name || item.title || '未命名'}

+

+ ${item.is_pinned ? '' : ''} + ${item.name || item.title || '未命名'} +

${fields}
-
- ${item.created_at ? item.created_at.split(' ')[0] : ''} +
+
+ ${item.publish_date || (item.created_at ? item.created_at.split(' ')[0] : '')} +
+ ${item.views ? `
${item.views}
` : ''}
@@ -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(); diff --git a/templates/models.html b/templates/models.html index b7bfcb3..711fbfe 100644 --- a/templates/models.html +++ b/templates/models.html @@ -42,14 +42,18 @@ oninput="loadModels()">