@@ -130,6 +130,9 @@
< 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-blue-50 rounded-lg p-4 mb-4" >
< p class = "text-sm text-blue-700" > < i class = "ri-information-line mr-1" > < / i > 内置分类( AI模型、GPU、CPU) 的子类别配置可在此编辑, 其数据管理入口在左侧导航栏的独立页面。< / p >
< / div >
< div class = "bg-white rounded-xl shadow-sm overflow-hidden" >
< table class = "w-full" >
< thead class = "bg-gray-50 border-b" >
@@ -137,8 +140,8 @@
< 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-center 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 >
@@ -156,6 +159,11 @@
< 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 >
< div class = "bg-gray-50 rounded-lg p-3 mb-4 flex items-center gap-4" id = "dynamic-filter-area" >
< span class = "text-sm text-gray-600" > 子类别筛选:< / span >
< button onclick = "filterDynamicBySubcategory('')" class = "px-3 py-1 rounded text-sm bg-white border text-gray-600 hover:bg-gray-100" > 全部< / button >
< div id = "dynamic-subcategory-filters" class = "flex gap-2" > < / div >
< / 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 >
@@ -170,6 +178,11 @@
< 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-gray-50 rounded-lg p-3 mb-4 flex items-center gap-4" >
< span class = "text-sm text-gray-600" > 子类别筛选:< / span >
< button onclick = "filterModelsBySubcategory('')" class = "px-3 py-1 rounded text-sm bg-white border text-gray-600 hover:bg-gray-100" > 全部< / button >
< div id = "model-subcategory-filters" class = "flex gap-2" > < / div >
< / div >
< 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" >
@@ -177,18 +190,17 @@
< 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-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 = "12 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< tbody id = "admin-models-table" > < tr > < td colspan = "11 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< / table >
< / div >
< / section >
@@ -202,6 +214,11 @@
< 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-gray-50 rounded-lg p-3 mb-4 flex items-center gap-4" >
< span class = "text-sm text-gray-600" > 子类别筛选:< / span >
< button onclick = "filterGpusBySubcategory('')" class = "px-3 py-1 rounded text-sm bg-white border text-gray-600 hover:bg-gray-100" > 全部< / button >
< div id = "gpu-subcategory-filters" class = "flex gap-2" > < / div >
< / div >
< 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" >
@@ -209,18 +226,17 @@
< 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-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 = "12 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< tbody id = "admin-gpus-table" > < tr > < td colspan = "11 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< / table >
< / div >
< / section >
@@ -234,6 +250,11 @@
< 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-gray-50 rounded-lg p-3 mb-4 flex items-center gap-4" >
< span class = "text-sm text-gray-600" > 子类别筛选:< / span >
< button onclick = "filterCpusBySubcategory('')" class = "px-3 py-1 rounded text-sm bg-white border text-gray-600 hover:bg-gray-100" > 全部< / button >
< div id = "cpu-subcategory-filters" class = "flex gap-2" > < / div >
< / div >
< 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" >
@@ -241,18 +262,17 @@
< 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-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 = "12 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< tbody id = "admin-cpus-table" > < tr > < td colspan = "11 " class = "text-center text-gray-400 py-8" > 加载中...< / td > < / tr > < / tbody >
< / table >
< / div >
< / section >
@@ -445,6 +465,46 @@
let categories = [ ] ;
let currentFilter = '' ;
let dynamicCategoryId = '' ;
let modelSubcategoryFilter = '' ;
let gpuSubcategoryFilter = '' ;
let cpuSubcategoryFilter = '' ;
let dynamicSubcategoryFilter = '' ;
// 生成随机ID( 12位十六进制)
function generateId ( ) {
return Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) + Math . random ( ) . toString ( 16 ) . slice ( 2 , 8 ) ;
}
// 获取子类别名称
function getSubcategoryName ( categoryId , subcategoryId ) {
const cat = categories . find ( c => c . id === categoryId ) ;
if ( ! cat || ! cat . subcategories ) return '-' ;
const sub = cat . subcategories . find ( s => s . id === subcategoryId ) ;
return sub ? sub . name : '-' ;
}
// 获取子类别图标
function getSubcategoryIcon ( categoryId , subcategoryId ) {
const cat = categories . find ( c => c . id === categoryId ) ;
if ( ! cat || ! cat . subcategories ) return '' ;
const sub = cat . subcategories . find ( s => s . id === subcategoryId ) ;
return sub ? sub . icon : '' ;
}
// 渲染子类别筛选按钮
function renderSubcategoryFilters ( categoryId , containerId , filterFunction ) {
const cat = categories . find ( c => c . id === categoryId ) ;
const container = document . getElementById ( containerId ) ;
if ( ! cat || ! cat . subcategories || cat . subcategories . length === 0 ) {
container . innerHTML = '' ;
return ;
}
container . innerHTML = cat . subcategories . map ( sub => `
<button onclick=" ${ filterFunction } (' ${ sub . id } ')" class="px-3 py-1 rounded text-sm bg-white border text-gray-600 hover:bg-gray-100">
<i class=" ${ sub . icon } mr-1"></i> ${ sub . name }
</button>
` ) . join ( '' ) ;
}
const colorMap = {
blue : 'bg-blue-100 text-blue-600' ,
@@ -574,6 +634,7 @@
// 显示动态分类数据
async function showDynamicCategory ( categoryId ) {
dynamicCategoryId = categoryId ;
dynamicSubcategoryFilter = '' ; // 重置筛选
const cat = categories . find ( c => c . id === categoryId ) ;
document . querySelectorAll ( 'section' ) . forEach ( s => s . classList . add ( 'hidden' ) ) ;
@@ -591,21 +652,44 @@
document . getElementById ( 'dynamic-title' ) . textContent = cat . name + '管理' ;
// 渲染子类别筛选按钮
if ( cat . subcategories && cat . subcategories . length > 0 ) {
document . getElementById ( 'dynamic-filter-area' ) . classList . remove ( 'hidden' ) ;
renderSubcategoryFilters ( categoryId , 'dynamic-subcategory-filters' , 'filterDynamicBySubcategory' ) ;
} else {
document . getElementById ( 'dynamic-filter-area' ) . classList . add ( 'hidden' ) ;
}
// 加载该分类的数据(后台显示全部,包括隐藏的)
const res = await fetch ( ` /api/items/ ${ categoryId } ?all=1 ` ) ;
cons t items = await res . json ( ) ;
le t items = await res . json ( ) ;
// 子类别筛选
if ( dynamicSubcategoryFilter ) {
items = items . filter ( i => i . subcategory _id === dynamicSubcategoryFilter ) ;
}
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' , 'visible' , 'raw_text' ] . includes ( k ) ) ;
const keys = Object . keys ( items [ 0 ] ) . filter ( k => ! [ 'id' , 'created_at' , 'updated_at' , 'visible' , 'raw_text' , 'subcategory_id' ]. includes ( k ) ) ;
let html = ` <thead class="bg-gray-50 border-b"><tr> ` ;
// 添加子类别列
if ( cat . subcategories && cat . subcategories . length > 0 ) {
html += ` <th class="px-4 py-3 text-left text-sm font-medium text-gray-600">子类别</th> ` ;
}
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> ` ;
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 ${ item . visible === false ? 'bg-gray-100 opacity-60' : '' } "> ` ;
// 子类别显示
if ( cat . subcategories && cat . subcategories . length > 0 ) {
const subName = getSubcategoryName ( categoryId , item . subcategory _id ) ;
const subIcon = getSubcategoryIcon ( categoryId , item . subcategory _id ) ;
html += ` <td class="px-4 py-3"> ${ item . subcategory _id ? ` <span class="px-2 py-1 bg-indigo-100 text-indigo-600 rounded text-xs"><i class=" ${ subIcon } mr-1"></i> ${ subName } </span> ` : '<span class="text-gray-400">-</span>' } </td> ` ;
}
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="toggleVisible('dynamic', ' ${ item . id } ')" class=" ${ item . visible === false ? 'text-gray-400' : 'text-green-600' } hover:opacity-80" title=" ${ item . visible === false ? '点击显示' : '点击隐藏' } ">
@@ -622,6 +706,11 @@
document . getElementById ( 'admin-dynamic-table' ) . innerHTML = html ;
}
}
function filterDynamicBySubcategory ( subId ) {
dynamicSubcategoryFilter = subId ;
showDynamicCategory ( dynamicCategoryId ) ;
}
// 切换显示区域
function showSection ( section ) {
@@ -754,6 +843,9 @@
: '<div class="text-gray-400">暂无数据</div>' ;
}
// 内置分类列表
const builtinCategories = [ 'ai-models' , 'gpus' , 'cpus' ] ;
// 加载分类列表
async function loadAdminCategories ( ) {
const res = await fetch ( '/api/categories?all=1' ) ;
@@ -764,30 +856,41 @@
return ;
}
document . getElementById ( 'admin-categories-table' ) . innerHTML = categories . map ( c => `
<tr class="border-b hover:bg-gray-50 ${ c . visible === false ? 'bg-gray-100 opacity-60' : '' } ">
document . getElementById ( 'admin-categories-table' ) . innerHTML = categories . map ( c => {
const isBuiltin = builtinCategories . includes ( c . id ) ;
const subcatCount = ( c . subcategories || [ ] ) . length ;
return `
<tr class="border-b hover:bg-gray-50 ${ c . visible === false ? 'bg-gray-100 opacity-60' : '' } ${ isBuiltin ? 'bg-indigo-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 text-center">
<button onclick="toggleVisible('category', ' ${ 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>
<td class="px-4 py-3 text-sm" >
${ isBuiltin ? '<span class="px-2 py-1 bg-indigo-100 text-indigo-600 rounded text-xs">内置</span>' : '<span class="text-gray-500">自定义</span>' }
</td >
<td class="px-4 py-3 text-sm" >
${ subcatCount > 0 ? ` <span class="px-2 py-1 bg-green-100 text-green-600 rounded text-xs"> ${ subcatCount } 个</span> ` : '<span class="text-gray-400">无</span>' }
</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>
<button onclick="editItem('category', ' ${ c . id } ')" class="text-blue-600 hover:text-blue-800 mr-2" title="编辑子类别" ><i class="ri-edit-line"></i></button>
${ ! isBuiltin ? ` <button onclick="deleteItem('category', ' ${ c . id } ')" class="text-red-600 hover:text-red-800" title="删除" ><i class="ri-delete-bin-line"></i></button>` : '<span class="text-gray-300 cursor-not-allowed"><i class="ri-delete-bin-line"></i></span>' }
</td>
</tr>
`) . join ( '' ) ;
` ;
} ) . join ( '' ) ;
}
// 加载模型列表
async function loadAdminModels ( ) {
renderSubcategoryFilters ( 'ai-models' , 'model-subcategory-filters' , 'filterModelsBySubcategory' ) ;
const res = await fetch ( '/api/models?all=1' ) ;
cons t models = await res . json ( ) ;
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 ; }
le t models = await res . json ( ) ;
// 子类别筛选
if ( modelSubcategoryFilter ) {
models = models . filter ( m => m . subcategory _id === modelSubcategoryFilter ) ;
}
if ( models . length === 0 ) { document . getElementById ( 'admin-models-table' ) . innerHTML = '<tr><td colspan="11" 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' : '' } ${ m . is _pinned ? 'bg-yellow-50' : '' } ">
<td class="px-3 py-3 text-center">
@@ -797,13 +900,14 @@
</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 . subcategory _id ? ` <span class="px-2 py-1 bg-blue-100 text-blue-600 rounded text-xs"><i class=" ${ getSubcategoryIcon ( 'ai-models' , m . subcategory _id ) } mr-1"></i> ${ getSubcategoryName ( 'ai-models' , m . subcategory _id ) } </span> ` : '<span class="text-gray-400">-</span>' }
</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>
@@ -817,12 +921,24 @@
</tr>
` ) . join ( '' ) ;
}
function filterModelsBySubcategory ( subId ) {
modelSubcategoryFilter = subId ;
loadAdminModels ( ) ;
}
// 加载GPU列表
async function loadAdminGpus ( ) {
renderSubcategoryFilters ( 'gpus' , 'gpu-subcategory-filters' , 'filterGpusBySubcategory' ) ;
const res = await fetch ( '/api/gpus?all=1' ) ;
cons t gpus = await res . json ( ) ;
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 ; }
le t gpus = await res . json ( ) ;
// 子类别筛选
if ( gpuSubcategoryFilter ) {
gpus = gpus . filter ( g => g . subcategory _id === gpuSubcategoryFilter ) ;
}
if ( gpus . length === 0 ) { document . getElementById ( 'admin-gpus-table' ) . innerHTML = '<tr><td colspan="11" 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' : '' } ${ g . is _pinned ? 'bg-yellow-50' : '' } ">
<td class="px-3 py-3 text-center">
@@ -832,13 +948,14 @@
</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 . subcategory _id ? ` <span class="px-2 py-1 bg-green-100 text-green-600 rounded text-xs"><i class=" ${ getSubcategoryIcon ( 'gpus' , g . subcategory _id ) } mr-1"></i> ${ getSubcategoryName ( 'gpus' , g . subcategory _id ) } </span> ` : '<span class="text-gray-400">-</span>' }
</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>
@@ -852,12 +969,24 @@
</tr>
` ) . join ( '' ) ;
}
function filterGpusBySubcategory ( subId ) {
gpuSubcategoryFilter = subId ;
loadAdminGpus ( ) ;
}
// 加载CPU列表
async function loadAdminCpus ( ) {
renderSubcategoryFilters ( 'cpus' , 'cpu-subcategory-filters' , 'filterCpusBySubcategory' ) ;
const res = await fetch ( '/api/cpus?all=1' ) ;
cons t cpus = await res . json ( ) ;
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 ; }
le t cpus = await res . json ( ) ;
// 子类别筛选
if ( cpuSubcategoryFilter ) {
cpus = cpus . filter ( c => c . subcategory _id === cpuSubcategoryFilter ) ;
}
if ( cpus . length === 0 ) { document . getElementById ( 'admin-cpus-table' ) . innerHTML = '<tr><td colspan="11" 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' : '' } ${ c . is _pinned ? 'bg-yellow-50' : '' } ">
<td class="px-3 py-3 text-center">
@@ -867,13 +996,14 @@
</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 . subcategory _id ? ` <span class="px-2 py-1 bg-purple-100 text-purple-600 rounded text-xs"><i class=" ${ getSubcategoryIcon ( 'cpus' , c . subcategory _id ) } mr-1"></i> ${ getSubcategoryName ( 'cpus' , c . subcategory _id ) } </span> ` : '<span class="text-gray-400">-</span>' }
</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>
@@ -887,6 +1017,11 @@
</tr>
` ) . join ( '' ) ;
}
function filterCpusBySubcategory ( subId ) {
cpuSubcategoryFilter = subId ;
loadAdminCpus ( ) ;
}
// 加载知识列表
async function loadAdminKnowledge ( ) {
@@ -1176,14 +1311,53 @@
// 表单模板
function getCategoryForm ( data = { } ) {
const subcategories = data . subcategories || [ ] ;
const isBuiltin = builtinCategories . includes ( data . id ) ;
// 存储到全局变量,便于管理
window . currentEditingSubcategories = JSON . parse ( JSON . stringify ( subcategories ) ) ;
// 内置类别只显示子类别管理
if ( isBuiltin ) {
return ` <form id="itemForm" class="space-y-4">
<div class="bg-indigo-50 rounded-lg p-4 mb-4">
<p class="text-sm text-indigo-700"><i class="ri-information-line mr-1"></i>内置分类的基础信息不可修改,只可编辑子类别配置。</p>
</div>
<div class="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
<div><label class="text-sm text-gray-500 mb-1 block">ID</label><div class="text-gray-700 font-mono"> ${ data . id } </div></div>
<div><label class="text-sm text-gray-500 mb-1 block">名称</label><div class="text-gray-700"> ${ data . name } </div></div>
<div><label class="text-sm text-gray-500 mb-1 block">图标</label><div class="text-gray-700"><i class=" ${ data . icon } mr-1"></i> ${ data . icon } </div></div>
<div><label class="text-sm text-gray-500 mb-1 block">颜色</label><div class="text-gray-700"> ${ data . color } </div></div>
</div>
<input type="hidden" name="id" value=" ${ data . id } ">
<input type="hidden" name="name" value=" ${ data . name } ">
<input type="hidden" name="icon" value=" ${ data . icon } ">
<input type="hidden" name="color" value=" ${ data . color } ">
<input type="hidden" name="order" value=" ${ data . order || 0 } ">
<input type="hidden" name="visible" value=" ${ data . visible !== false ? 'true' : 'false' } ">
<input type="hidden" name="description" value=" ${ data . description || '' } ">
<!-- 子类别管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-700"><i class="ri-folder-line mr-1"></i>子类别管理</label>
<button onclick="openSubcategoryAddModal()" class="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700">
<i class="ri-add-line mr-1"></i>添加子类别
</button>
</div>
<div id="subcategoriesList" class="space-y-2">
${ renderSubcategoriesList ( subcategories ) }
</div>
<input type="hidden" name="subcategories" id="subcategoriesHidden" value=' ${ JSON . stringify ( subcategories ) } '>
</div>
</form> ` ;
}
// 自定义类别完整编辑表单
const autoId = data . id || generateId ( ) ;
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-1 00' : '' } " ></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">ID</label><input type="text" name="id" value=" ${ autoId } " readonly class="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-500 font-mono text-xs"><p class="text-xs text -gray-4 00 mt-1">自动生成,无需填写</p ></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" placeholder="如:手机、电脑" ></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" placeholder="ri-folder-line" ></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>
@@ -1198,7 +1372,7 @@
<option value="false" ${ data . visible === false ? 'selected' : '' } >隐藏</option>
</select></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>
<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" placeholder="分类描述" > ${ data . description || '' } </textarea></div>
<!-- 子类别管理 -->
<div class="border-t pt-4">
@@ -1275,11 +1449,12 @@
const keyFeatures = ( data . key _features || [ ] ) . join ( ', ' ) ;
const featureLabels = data . feature _labels || { } ;
const featureLabelsStr = Object . entries ( featureLabels ) . map ( ( [ k , v ] ) => ` ${ k } : ${ v } ` ) . join ( ', ' ) ;
const autoSubId = data . id || generateId ( ) ;
return ` <div 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" id="sub_id" value=" ${ data . id || '' } " 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" id="sub_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">ID</label><input type="text" id="sub_id" value=" ${ autoSubId } " readonly class="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-500 font-mono text-xs"><p class="text-xs text-gray-400 mt-1">自动生成</p ></div>
<div><label class="text-sm text-gray-600 mb-1 block">名称 *</label><input type="text" id="sub_name" value=" ${ data . name || '' } " required class="w-full px-3 py-2 border rounded-lg" placeholder="如:旗舰手机" ></div>
<div><label class="text-sm text-gray-600 mb-1 block">图标</label><input type="text" id="sub_icon" value=" ${ data . icon || 'ri-folder-line' } " class="w-full px-3 py-2 border rounded-lg" placeholder="ri-folder-line"></div>
</div>
<div>
@@ -1303,11 +1478,13 @@
const keyFeaturesStr = document . getElementById ( 'sub_key_features' ) . value . trim ( ) ;
const featureLabelsStr = document . getElementById ( 'sub_feature_labels' ) . value . trim ( ) ;
if ( ! id || ! name) {
alert ( 'ID和 名称不能为空' ) ;
if ( ! name ) {
alert ( '名称不能为空' ) ;
return ;
}
// ID自动生成, 无需校验
// 解析 key_features
const key _features = keyFeaturesStr ? keyFeaturesStr . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( s => s ) : [ ] ;
@@ -1364,11 +1541,28 @@
</form> ` ;
}
// 生成子类别选择器
function getSubcategorySelect ( categoryId , currentValue = '' ) {
const cat = categories . find ( c => c . id === categoryId ) ;
if ( ! cat || ! cat . subcategories || cat . subcategories . length === 0 ) {
return '' ;
}
const options = cat . subcategories . map ( sub =>
` <option value=" ${ sub . id } " ${ currentValue === sub . id ? 'selected' : '' } ><i class=" ${ sub . icon } mr-1"></i> ${ sub . name } </option> `
) . join ( '' ) ;
return ` <div><label class="text-sm text-gray-600 mb-1 block">子类别</label><select name="subcategory_id" class="w-full px-3 py-2 border rounded-lg"><option value="">请选择</option> ${ options } </select></div> ` ;
}
function getDynamicForm ( data = { } ) {
const cat = categories . find ( c => c . id === dynamicCategoryId ) ;
currentImages = data . images || [ ] ;
const subcategorySelect = getSubcategorySelect ( dynamicCategoryId , data . subcategory _id ) ;
return ` <form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
${ subcategorySelect }
<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>
@@ -1384,8 +1578,11 @@
function getModelForm ( data = { } ) {
currentImages = data . images || [ ] ;
const subcategorySelect = getSubcategorySelect ( 'ai-models' , data . subcategory _id ) ;
return ` <form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
${ subcategorySelect }
<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>
@@ -1407,8 +1604,11 @@
function getGpuForm ( data = { } ) {
currentImages = data . images || [ ] ;
const subcategorySelect = getSubcategorySelect ( 'gpus' , data . subcategory _id ) ;
return ` <form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
${ subcategorySelect }
<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>
@@ -1436,8 +1636,11 @@
function getCpuForm ( data = { } ) {
currentImages = data . images || [ ] ;
const subcategorySelect = getSubcategorySelect ( 'cpus' , data . subcategory _id ) ;
return ` <form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
${ subcategorySelect }
<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>