Files
param-hub-python/templates/admin.html
hubian fc0e8c3443 fix: 修复后台管理多个问题
修复:
1. 分类编辑功能正常工作
2. 动态生成侧边栏菜单,新增分类自动出现管理入口
3. 首页动态加载分类并展示
4. 新增动态分类数据管理API
5. 修复CPU等页面图标问题
2026-04-09 02:32:35 +08:00

730 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理 - ParamHub</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.sidebar-link { transition: all 0.2s; }
.sidebar-link:hover { background: #374151; }
.sidebar-link.active { background: #4f46e5; color: white; }
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- 侧边栏 -->
<aside class="fixed left-0 top-0 w-64 h-full bg-gray-800 text-white overflow-y-auto">
<div class="p-4 border-b border-gray-700">
<div class="flex items-center gap-2">
<i class="ri-dashboard-3-line text-2xl"></i>
<span class="text-xl font-bold">ParamHub</span>
</div>
<div class="text-sm text-gray-400 mt-1">后台管理</div>
</div>
<nav class="p-4">
<div class="space-y-1" id="sidebarNav">
<!-- 动态生成 -->
</div>
<div class="mt-8 pt-4 border-t border-gray-700">
<a href="/" target="_blank" class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-700 text-gray-400">
<i class="ri-external-link-line"></i>
<span>访问前台</span>
</a>
</div>
</nav>
</aside>
<!-- 主内容 -->
<main class="ml-64 p-8">
<!-- 概览 -->
<section id="section-overview">
<h1 class="text-2xl font-bold text-gray-800 mb-6">管理概览</h1>
<div class="grid grid-cols-5 gap-4 mb-8" id="statsCards"><div class="text-center text-gray-400 py-4">加载中...</div></div>
<div class="bg-white rounded-xl p-6 shadow-sm mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2>
<div class="grid grid-cols-5 gap-3" id="quickActions"><div class="text-gray-400">加载中...</div></div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm">
<h2 class="text-lg font-semibold text-gray-800 mb-4">最近添加的模型</h2>
<div id="recent-models" class="space-y-2">加载中...</div>
</div>
</section>
<!-- 分类管理 -->
<section id="section-categories" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">分类管理</h1>
<button onclick="openAddModal('category')" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-add-line mr-2"></i>添加分类</button>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<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">ID</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>
</tr>
</thead>
<tbody id="admin-categories-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- 动态分类数据管理容器 -->
<section id="section-dynamic" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1>
<button onclick="openAddModal('dynamic')" 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 class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full"><tbody id="admin-dynamic-table"><tr><td class="text-center text-gray-400 py-8">加载中...</td></tr></tbody></table>
</div>
</section>
<!-- 模型管理 -->
<section id="section-models" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">模型管理</h1>
<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 class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<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>
</tr>
</thead>
<tbody id="admin-models-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- GPU管理 -->
<section id="section-gpus" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">GPU管理</h1>
<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>添加GPU</button>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<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>
</tr>
</thead>
<tbody id="admin-gpus-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- CPU管理 -->
<section id="section-cpus" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">CPU管理</h1>
<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>添加CPU</button>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<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>
</tr>
</thead>
<tbody id="admin-cpus-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- 知识库管理 -->
<section id="section-knowledge" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">知识库管理</h1>
<button onclick="openAddModal('knowledge')" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-add-line mr-2"></i>添加知识</button>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">筛选分类:</span>
<button onclick="filterKnowledge('')" class="px-3 py-1 rounded text-sm bg-gray-100 text-gray-600 hover:bg-gray-200">全部</button>
<div id="category-filters" class="flex gap-2"></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<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>
</tr>
</thead>
<tbody id="admin-knowledge-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
</main>
<!-- 编辑弹窗 -->
<div id="editModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-auto">
<div class="p-6 border-b flex justify-between items-center sticky top-0 bg-white z-10">
<h2 class="text-xl font-bold text-gray-800" id="modalTitle">添加/编辑</h2>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div id="modalContent" class="p-6"></div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="saveItem()" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">保存</button>
</div>
</div>
</div>
<script>
let currentType = '';
let currentId = '';
let currentData = {};
let categories = [];
let currentFilter = '';
let dynamicCategoryId = '';
const colorMap = {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
teal: 'bg-teal-100 text-teal-600',
red: 'bg-red-100 text-red-600'
};
// 初始化
async function init() {
await loadCategories();
renderSidebar();
loadOverview();
}
// 加载分类
async function loadCategories() {
const res = await fetch('/api/categories');
categories = await res.json();
categories.sort((a, b) => (a.order || 0) - (b.order || 0));
}
// 渲染侧边栏
function renderSidebar() {
const fixedItems = [
{id: 'overview', name: '概览', icon: 'ri-home-4-line'},
{id: 'categories', name: '分类管理', icon: 'ri-folder-line'},
];
// 内置分类映射
const builtinMap = {
'ai-models': {id: 'models', name: '模型管理', icon: 'ri-robot-line'},
'gpus': {id: 'gpus', name: 'GPU管理', icon: 'ri-cpu-line'},
'cpus': {id: 'cpus', name: 'CPU管理', icon: 'ri-memory-line'}
};
let html = fixedItems.map(item => `
<a href="#${item.id}" onclick="showSection('${item.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg ${item.id === 'overview' ? 'active text-white' : 'text-gray-300'}" data-section="${item.id}">
<i class="${item.icon}"></i>
<span>${item.name}</span>
</a>
`).join('');
// 添加每个分类的管理入口
categories.forEach(cat => {
const builtin = builtinMap[cat.id];
if (builtin) {
html += `
<a href="#${builtin.id}" onclick="showSection('${builtin.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="${builtin.id}">
<i class="${builtin.icon}"></i>
<span>${builtin.name}</span>
</a>
`;
} else {
// 动态分类
html += `
<a href="#cat-${cat.id}" onclick="showDynamicCategory('${cat.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="cat-${cat.id}">
<i class="${cat.icon}"></i>
<span>${cat.name}管理</span>
</a>
`;
}
});
// 知识库管理
html += `
<a href="#knowledge" onclick="showSection('knowledge')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="knowledge">
<i class="ri-book-open-line"></i>
<span>知识库管理</span>
</a>
`;
document.getElementById('sidebarNav').innerHTML = html;
}
// 显示动态分类数据
async function showDynamicCategory(categoryId) {
dynamicCategoryId = categoryId;
const cat = categories.find(c => c.id === categoryId);
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
document.getElementById('section-dynamic').classList.remove('hidden');
document.querySelectorAll('.sidebar-link').forEach(a => {
a.classList.remove('active', 'text-white');
a.classList.add('text-gray-300');
});
const activeLink = document.querySelector(`.sidebar-link[data-section="cat-${categoryId}"]`);
if (activeLink) {
activeLink.classList.add('active', 'text-white');
activeLink.classList.remove('text-gray-300');
}
document.getElementById('dynamic-title').textContent = cat.name + '管理';
// 加载该分类的数据
const res = await fetch(`/api/items/${categoryId}`);
const items = await res.json();
if (items.length === 0) {
document.getElementById('admin-dynamic-table').innerHTML = '<tr><td class="text-center text-gray-400 py-8">暂无数据,点击上方"添加数据"按钮添加</td></tr>';
} else {
const keys = Object.keys(items[0]).filter(k => !['id', 'created_at', 'updated_at'].includes(k));
let html = `<thead class="bg-gray-50 border-b"><tr>`;
keys.forEach(k => { html += `<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">${k}</th>`; });
html += `<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th></tr></thead><tbody>`;
items.forEach(item => {
html += `<tr class="border-b hover:bg-gray-50">`;
keys.forEach(k => { html += `<td class="px-4 py-3 text-gray-600">${item[k] || '-'}</td>`; });
html += `<td class="px-4 py-3 text-center">
<button onclick="editDynamicItem('${item.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteDynamicItem('${item.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td></tr>`;
});
html += '</tbody>';
document.getElementById('admin-dynamic-table').innerHTML = html;
}
}
// 切换显示区域
function showSection(section) {
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
document.getElementById('section-' + section).classList.remove('hidden');
document.querySelectorAll('.sidebar-link').forEach(a => {
a.classList.remove('active', 'text-white');
a.classList.add('text-gray-300');
});
const activeLink = document.querySelector(`.sidebar-link[data-section="${section}"]`);
if (activeLink) {
activeLink.classList.add('active', 'text-white');
activeLink.classList.remove('text-gray-300');
}
if (section === 'categories') loadAdminCategories();
if (section === 'models') loadAdminModels();
if (section === 'gpus') loadAdminGpus();
if (section === 'cpus') loadAdminCpus();
if (section === 'knowledge') loadAdminKnowledge();
}
// 加载概览统计
async function loadOverview() {
const res = await fetch('/api/stats');
const data = await res.json();
const statsHtml = `
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center"><i class="ri-folder-line text-xl text-blue-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.categories_count}</div><div class="text-xs text-gray-500">分类数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center"><i class="ri-robot-line text-xl text-indigo-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.models_count}</div><div class="text-xs text-gray-500">模型数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center"><i class="ri-cpu-line text-xl text-green-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.gpus_count}</div><div class="text-xs text-gray-500">GPU数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center"><i class="ri-memory-line text-xl text-purple-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.cpus_count}</div><div class="text-xs text-gray-500">CPU数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-teal-100 flex items-center justify-center"><i class="ri-book-open-line text-xl text-teal-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.knowledge_count}</div><div class="text-xs text-gray-500">知识条目</div></div>
</div>
</div>
`;
document.getElementById('statsCards').innerHTML = statsHtml;
// 快捷操作
const actionsHtml = `
<button onclick="showSection('categories'); openAddModal('category')" class="px-4 py-3 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加分类</button>
<button onclick="showSection('models'); openAddModal('model')" class="px-4 py-3 bg-indigo-50 text-indigo-600 rounded-lg hover:bg-indigo-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加模型</button>
<button onclick="showSection('gpus'); openAddModal('gpu')" class="px-4 py-3 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加GPU</button>
<button onclick="showSection('cpus'); openAddModal('cpu')" class="px-4 py-3 bg-purple-50 text-purple-600 rounded-lg hover:bg-purple-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加CPU</button>
<button onclick="showSection('knowledge'); openAddModal('knowledge')" class="px-4 py-3 bg-teal-50 text-teal-600 rounded-lg hover:bg-teal-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加知识</button>
`;
document.getElementById('quickActions').innerHTML = actionsHtml;
const models = data.latest_models || [];
document.getElementById('recent-models').innerHTML = models.length > 0
? models.map(m => `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"><div><span class="font-medium text-gray-800">${m.name}</span><span class="text-sm text-gray-500 ml-2">${m.organization}</span></div><div class="text-sm text-gray-400">${m.is_open_source ? '开源' : '商业'}</div></div>`).join('')
: '<div class="text-gray-400">暂无数据</div>';
}
// 加载分类列表
async function loadAdminCategories() {
const res = await fetch('/api/categories');
categories = await res.json();
if (categories.length === 0) {
document.getElementById('admin-categories-table').innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-8">暂无数据</td></tr>';
return;
}
document.getElementById('admin-categories-table').innerHTML = categories.map(c => `
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3"><div class="w-10 h-10 rounded-lg ${colorMap[c.color] || 'bg-gray-100 text-gray-600'} flex items-center justify-center"><i class="${c.icon} text-xl"></i></div></td>
<td class="px-4 py-3 text-gray-500 text-sm font-mono">${c.id}</td>
<td class="px-4 py-3 font-medium text-gray-800">${c.name}</td>
<td class="px-4 py-3 text-gray-600 text-sm">${c.description || '-'}</td>
<td class="px-4 py-3">${c.order || 0}</td>
<td class="px-4 py-3 text-center">
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('category', '${c.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`).join('');
}
// 加载模型列表
async function loadAdminModels() {
const res = await fetch('/api/models');
const models = await res.json();
if (models.length === 0) { document.getElementById('admin-models-table').innerHTML = '<tr><td colspan="6" 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">
<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">
<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>
</tr>
`).join('');
}
// 加载GPU列表
async function loadAdminGpus() {
const res = await fetch('/api/gpus');
const gpus = await res.json();
if (gpus.length === 0) { document.getElementById('admin-gpus-table').innerHTML = '<tr><td colspan="6" 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">
<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">$${g.price_usd || '-'}</td>
<td class="px-4 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>
</tr>
`).join('');
}
// 加载CPU列表
async function loadAdminCpus() {
const res = await fetch('/api/cpus');
const cpus = await res.json();
if (cpus.length === 0) { document.getElementById('admin-cpus-table').innerHTML = '<tr><td colspan="6" 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">
<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">$${c.price_usd || '-'}</td>
<td class="px-4 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>
</tr>
`).join('');
}
// 加载知识列表
async function loadAdminKnowledge() {
const catRes = await fetch('/api/categories');
categories = await catRes.json();
document.getElementById('category-filters').innerHTML = categories.map(c => `<button onclick="filterKnowledge('${c.id}')" class="px-3 py-1 rounded text-sm bg-gray-100 text-gray-600 hover:bg-gray-200">${c.name}</button>`).join('');
let url = '/api/knowledge';
if (currentFilter) url += `?category=${currentFilter}`;
const res = await fetch(url);
const knowledge = await res.json();
if (knowledge.length === 0) { document.getElementById('admin-knowledge-table').innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
document.getElementById('admin-knowledge-table').innerHTML = knowledge.map(k => {
const cat = categories.find(c => c.id === k.category);
return `<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3"><i class="${k.icon || 'ri-file-list-line'} text-xl text-gray-400"></i></td>
<td class="px-4 py-3 font-medium text-gray-800">${k.title}</td>
<td class="px-4 py-3 text-gray-600">${cat ? cat.name : '-'}</td>
<td class="px-4 py-3 text-gray-500 text-sm max-w-xs truncate">${k.content || '-'}</td>
<td class="px-4 py-3">${k.order || 0}</td>
<td class="px-4 py-3 text-center">
<button onclick="editItem('knowledge', '${k.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('knowledge', '${k.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>`;
}).join('');
}
function filterKnowledge(category) { currentFilter = category; loadAdminKnowledge(); }
// 打开添加弹窗
function openAddModal(type) {
currentType = type;
currentId = '';
currentData = {};
const titles = {category: '分类', model: '模型', gpu: 'GPU', cpu: 'CPU', knowledge: '知识', dynamic: '数据'};
document.getElementById('modalTitle').textContent = '添加' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm, dynamic: getDynamicForm};
document.getElementById('modalContent').innerHTML = forms[type]();
document.getElementById('editModal').classList.remove('hidden');
}
// 编辑项
async function editItem(type, id) {
currentType = type;
currentId = id;
const endpoint = type === 'category' ? 'categories' : type === 'knowledge' ? 'knowledge' : type + 's';
const res = await fetch(`/api/${endpoint}/${id}`);
currentData = await res.json();
const titles = {category: '分类', model: '模型', gpu: 'GPU', cpu: 'CPU', knowledge: '知识'};
document.getElementById('modalTitle').textContent = '编辑' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm};
document.getElementById('modalContent').innerHTML = forms[type](currentData);
document.getElementById('editModal').classList.remove('hidden');
}
// 编辑动态分类数据
async function editDynamicItem(id) {
currentType = 'dynamic';
currentId = id;
const res = await fetch(`/api/items/${dynamicCategoryId}/${id}`);
currentData = await res.json();
document.getElementById('modalTitle').textContent = '编辑数据';
document.getElementById('modalContent').innerHTML = getDynamicForm(currentData);
document.getElementById('editModal').classList.remove('hidden');
}
// 删除项
async function deleteItem(type, id) {
if (!confirm('确定删除?')) return;
const endpoint = type === 'category' ? 'categories' : type === 'knowledge' ? 'knowledge' : type + 's';
await fetch(`/api/${endpoint}/${id}`, { method: 'DELETE' });
const loaders = {category: loadAdminCategories, model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus, knowledge: loadAdminKnowledge};
loaders[type]();
loadOverview();
if (type === 'category') { await loadCategories(); renderSidebar(); }
}
// 删除动态分类数据
async function deleteDynamicItem(id) {
if (!confirm('确定删除?')) return;
await fetch(`/api/items/${dynamicCategoryId}/${id}`, { method: 'DELETE' });
showDynamicCategory(dynamicCategoryId);
}
// 保存项
async function saveItem() {
const form = document.getElementById('itemForm');
const formData = new FormData(form);
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', 'release_year', 'cores', 'threads', 'base_clock_ghz', 'boost_clock_ghz', 'l3_cache_mb', 'tdp_watts', 'order'];
if (numFields.includes(key)) data[key] = parseFloat(value);
else if (key === 'is_open_source') data[key] = value === 'true';
else data[key] = value;
}
});
if (currentType === 'dynamic') {
data.category_id = dynamicCategoryId;
}
let endpoint;
if (currentType === 'category') endpoint = 'categories';
else if (currentType === 'knowledge') endpoint = 'knowledge';
else if (currentType === 'dynamic') endpoint = `items/${dynamicCategoryId}`;
else endpoint = currentType + 's';
if (currentId) {
await fetch(`/api/${endpoint}/${currentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
} else {
await fetch(`/api/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
}
closeModal();
if (currentType === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {category: loadAdminCategories, model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus, knowledge: loadAdminKnowledge};
loaders[currentType]();
}
loadOverview();
if (currentType === 'category') { await loadCategories(); renderSidebar(); }
}
function closeModal() { document.getElementById('editModal').classList.add('hidden'); }
// 表单模板
function getCategoryForm(data = {}) {
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">ID *</label><input type="text" name="id" value="${data.id || ''}" ${data.id ? 'readonly' : ''} required class="w-full px-3 py-2 border rounded-lg ${data.id ? 'bg-gray-100' : ''}"></div>
<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="icon" value="${data.icon || 'ri-folder-line'}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">颜色</label><select name="color" class="w-full px-3 py-2 border rounded-lg">
<option value="blue" ${data.color === 'blue' ? 'selected' : ''}>蓝色</option>
<option value="green" ${data.color === 'green' ? 'selected' : ''}>绿色</option>
<option value="purple" ${data.color === 'purple' ? 'selected' : ''}>紫色</option>
<option value="orange" ${data.color === 'orange' ? 'selected' : ''}>橙色</option>
<option value="teal" ${data.color === 'teal' ? 'selected' : ''}>青色</option>
<option value="red" ${data.color === 'red' ? 'selected' : ''}>红色</option>
</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">排序</label><input type="number" name="order" value="${data.order || 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="2" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
</form>`;
}
function getKnowledgeForm(data = {}) {
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="title" value="${data.title || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">分类</label><select name="category" class="w-full px-3 py-2 border rounded-lg"><option value="">选择分类</option>${categories.map(c => `<option value="${c.id}" ${data.category === c.id ? 'selected' : ''}>${c.name}</option>`).join('')}</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">图标</label><input type="text" name="icon" value="${data.icon || 'ri-file-list-line'}" 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="order" value="${data.order || 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="content" rows="2" required class="w-full px-3 py-2 border rounded-lg">${data.content || ''}</textarea></div>
<div><label class="text-sm text-gray-600 mb-1 block">详细内容</label><textarea name="detail" rows="6" class="w-full px-3 py-2 border rounded-lg">${data.detail || ''}</textarea></div>
</form>`;
}
function getDynamicForm(data = {}) {
const cat = categories.find(c => c.id === dynamicCategoryId);
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>
<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>
</form>`;
}
function getModelForm(data = {}) {
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="organization" value="${data.organization || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">参数量(B) *</label><input type="number" name="parameters" value="${data.parameters || ''}" 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="architecture" value="${data.architecture || ''}" 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="context_length" value="${data.context_length || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">类型</label><select name="is_open_source" class="w-full px-3 py-2 border rounded-lg"><option value="false" ${!data.is_open_source ? 'selected' : ''}>商业</option><option value="true" ${data.is_open_source ? 'selected' : ''}>开源</option></select></div>
<div><label class="text-sm text-gray-600 mb-1 block">MMLU分数</label><input type="number" name="mmlu" value="${data.mmlu || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">HumanEval分数</label><input type="number" name="humaneval" value="${data.humaneval || ''}" step="0.1" 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="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>
<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>
</form>`;
}
function getGpuForm(data = {}) {
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="manufacturer" value="${data.manufacturer || ''}" 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="architecture" value="${data.architecture || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">显存(GB) *</label><input type="number" name="memory_gb" value="${data.memory_gb || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">CUDA核心</label><input type="number" name="cuda_cores" value="${data.cuda_cores || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">Tensor核心</label><input type="number" name="tensor_cores" value="${data.tensor_cores || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">显存带宽(GB/s)</label><input type="number" name="memory_bandwidth_gbs" value="${data.memory_bandwidth_gbs || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">FP16性能(TF)</label><input type="number" name="fp16_tflops" value="${data.fp16_tflops || ''}" step="0.1" 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_usd" value="${data.price_usd || ''}" 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>
<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>
</form>`;
}
function getCpuForm(data = {}) {
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="manufacturer" value="${data.manufacturer || ''}" 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="architecture" value="${data.architecture || ''}" 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="cores" value="${data.cores || ''}" 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="number" name="threads" value="${data.threads || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">基础频率(GHz)</label><input type="number" name="base_clock_ghz" value="${data.base_clock_ghz || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">加速频率(GHz)</label><input type="number" name="boost_clock_ghz" value="${data.boost_clock_ghz || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">L3缓存(MB)</label><input type="number" name="l3_cache_mb" value="${data.l3_cache_mb || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">TDP功耗(W)</label><input type="number" name="tdp_watts" value="${data.tdp_watts || ''}" 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_usd" value="${data.price_usd || ''}" 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>
</form>`;
}
document.getElementById('editModal').addEventListener('click', function(e) { if (e.target === this) closeModal(); });
init();
</script>
</body>
</html>