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

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