feat: 支持子类别配置和关键特性显示

- 类别数据结构新增 subcategories 字段
- 每个子类别可定义 key_features 和 feature_labels
- 前端模型页面添加子类别选择器
- 表格根据子类别动态显示关键特性列
- 后台管理支持编辑子类别配置(JSON格式)
- 预设了各类别的子类别配置(对话、代码、推理、视觉等)
This commit is contained in:
2026-04-28 00:16:55 +08:00
parent 961322f8ba
commit 685582b7e6
3 changed files with 428 additions and 18 deletions

View File

@@ -5,7 +5,54 @@
"icon": "ri-robot-line",
"color": "blue",
"description": "大语言模型、图像模型等AI模型参数",
"order": 1
"order": 1,
"subcategories": [
{
"id": "chat",
"name": "对话模型",
"icon": "ri-chat-3-line",
"key_features": ["context_length", "mmlu", "input_price", "output_price"],
"feature_labels": {
"context_length": "上下文",
"mmlu": "MMLU",
"input_price": "输入价",
"output_price": "输出价"
}
},
{
"id": "code",
"name": "代码模型",
"icon": "ri-code-line",
"key_features": ["humaneval", "context_length", "input_price"],
"feature_labels": {
"humaneval": "HumanEval",
"context_length": "上下文",
"input_price": "输入价"
}
},
{
"id": "reasoning",
"name": "推理模型",
"icon": "ri-lightbulb-line",
"key_features": ["reasoning_capability", "mmlu", "context_length"],
"feature_labels": {
"reasoning_capability": "推理能力",
"mmlu": "MMLU",
"context_length": "上下文"
}
},
{
"id": "vision",
"name": "视觉模型",
"icon": "ri-image-line",
"key_features": ["vision_capability", "multimodal", "context_length"],
"feature_labels": {
"vision_capability": "视觉能力",
"multimodal": "多模态",
"context_length": "上下文"
}
}
]
},
{
"id": "gpus",
@@ -13,7 +60,45 @@
"icon": "ri-cpu-line",
"color": "green",
"description": "NVIDIA、AMD等GPU显卡规格参数",
"order": 2
"order": 2,
"subcategories": [
{
"id": "gaming",
"name": "游戏显卡",
"icon": "ri-gamepad-line",
"key_features": ["memory_gb", "cuda_cores", "price_usd", "fp16_tflops"],
"feature_labels": {
"memory_gb": "显存",
"cuda_cores": "CUDA核心",
"price_usd": "价格",
"fp16_tflops": "FP16性能"
}
},
{
"id": "professional",
"name": "专业显卡",
"icon": "ri-building-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "price_usd"],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
"memory_bandwidth_gbs": "带宽",
"price_usd": "价格"
}
},
{
"id": "datacenter",
"name": "数据中心",
"icon": "ri-server-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "fp16_tflops"],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
"memory_bandwidth_gbs": "带宽",
"fp16_tflops": "FP16性能"
}
}
]
},
{
"id": "cpus",
@@ -21,7 +106,45 @@
"icon": "ri-cpu-line",
"color": "purple",
"description": "Intel、AMD等CPU处理器参数",
"order": 3
"order": 3,
"subcategories": [
{
"id": "desktop",
"name": "桌面CPU",
"icon": "ri-computer-line",
"key_features": ["cores", "threads", "boost_clock_ghz", "price_usd"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"boost_clock_ghz": "加速频率",
"price_usd": "价格"
}
},
{
"id": "server",
"name": "服务器CPU",
"icon": "ri-server-line",
"key_features": ["cores", "threads", "l3_cache_mb", "tdp_watts"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"l3_cache_mb": "L3缓存",
"tdp_watts": "功耗"
}
},
{
"id": "mobile",
"name": "移动CPU",
"icon": "ri-smartphone-line",
"key_features": ["cores", "threads", "base_clock_ghz", "tdp_watts"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"base_clock_ghz": "基础频率",
"tdp_watts": "功耗"
}
}
]
},
{
"id": "phones",
@@ -30,7 +153,33 @@
"color": "orange",
"description": "各品牌手机参数规格",
"order": 4,
"visible": false
"visible": true,
"subcategories": [
{
"id": "flagship",
"name": "旗舰手机",
"icon": "ri-star-line",
"key_features": ["processor", "ram_gb", "storage_gb", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"storage_gb": "存储",
"price": "价格"
}
},
{
"id": "midrange",
"name": "中端手机",
"icon": "ri-price-tag-3-line",
"key_features": ["processor", "ram_gb", "battery_mah", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"battery_mah": "电池",
"price": "价格"
}
}
]
},
{
"id": "laptops",
@@ -38,7 +187,33 @@
"icon": "ri-macbook-line",
"color": "teal",
"description": "笔记本电脑、台式机参数",
"order": 5
"order": 5,
"subcategories": [
{
"id": "gaming-laptop",
"name": "游戏笔记本",
"icon": "ri-gamepad-line",
"key_features": ["processor", "gpu", "ram_gb", "price"],
"feature_labels": {
"processor": "处理器",
"gpu": "显卡",
"ram_gb": "内存",
"price": "价格"
}
},
{
"id": "business-laptop",
"name": "商务笔记本",
"icon": "ri-briefcase-line",
"key_features": ["processor", "ram_gb", "weight_kg", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"weight_kg": "重量",
"price": "价格"
}
}
]
},
{
"id": "021dc76d36be",
@@ -47,6 +222,66 @@
"color": "red",
"order": 6,
"description": "汽车方面",
"created_at": "2026-04-09 10:09:01"
"created_at": "2026-04-09 10:09:01",
"subcategories": [
{
"id": "sedan",
"name": "轿车",
"icon": "ri-car-line",
"key_features": ["engine", "power_kw", "price"],
"feature_labels": {
"engine": "发动机",
"power_kw": "功率",
"price": "价格"
}
},
{
"id": "suv",
"name": "SUV",
"icon": "ri-truck-line",
"key_features": ["engine", "seats", "price"],
"feature_labels": {
"engine": "发动机",
"seats": "座位数",
"price": "价格"
}
}
]
},
{
"id": "71fa2b4d818f",
"name": "摄像",
"icon": "ri-camera-line",
"color": "blue",
"order": 0,
"visible": true,
"description": "相机、摄像机等",
"created_at": "2026-04-25 16:38:47",
"subcategories": [
{
"id": "mirrorless",
"name": "无反相机",
"icon": "ri-camera-line",
"key_features": ["sensor", "megapixels", "video_resolution", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"video_resolution": "视频",
"price": "价格"
}
},
{
"id": "dslr",
"name": "单反相机",
"icon": "ri-camera-2-line",
"key_features": ["sensor", "megapixels", "lens_mount", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"lens_mount": "卡口",
"price": "价格"
}
}
]
}
]

View File

@@ -922,6 +922,13 @@
else if (key === 'images') {
try { data[key] = JSON.parse(value); } catch { data[key] = []; }
}
else if (key === 'subcategories') {
// 解析子类别JSON
try { data[key] = JSON.parse(value); } catch {
alert('子类别JSON格式错误请检查格式');
return;
}
}
else data[key] = value;
}
});
@@ -1096,6 +1103,9 @@
// 表单模板
function getCategoryForm(data = {}) {
const subcategories = data.subcategories || [];
const subcategoriesJson = JSON.stringify(subcategories, null, 2);
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>
@@ -1116,6 +1126,24 @@
</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 class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block"><i class="ri-folder-line mr-1"></i>子类别配置JSON格式</label>
<div class="bg-blue-50 rounded-lg p-3 mb-2 text-xs text-blue-700">
<p class="mb-2">子类别配置示例:</p>
<pre class="bg-blue-100 p-2 rounded overflow-x-auto">[
{
"id": "chat",
"name": "对话模型",
"icon": "ri-chat-3-line",
"key_features": ["context_length", "mmlu"],
"feature_labels": {"context_length": "上下文", "mmlu": "MMLU"}
}
]</pre>
</div>
<textarea name="subcategories" rows="8" class="w-full px-3 py-2 border rounded-lg font-mono text-sm" placeholder='[]'>${subcategoriesJson}</textarea>
</div>
</form>`;
}

View File

@@ -32,6 +32,19 @@
<p class="text-gray-500 mt-1">AI大模型参数规格一览</p>
</div>
<!-- 子类别选择器 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-600"><i class="ri-folder-line mr-1"></i>子类别:</span>
</div>
<div class="flex gap-2" id="subcategoryTabs">
<button onclick="selectSubcategory('')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm" id="subcat-all">
<i class="ri-apps-line mr-1"></i>全部
</button>
<!-- 动态加载子类别 -->
</div>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="flex gap-4 items-center">
@@ -101,12 +114,38 @@
<script>
let allModels = [];
let categories = [];
let currentCategory = null;
let currentSubcategory = '';
// 子类别默认特性配置
const DEFAULT_KEY_FEATURES = {
'chat': ['context_length', 'mmlu', 'input_price', 'output_price'],
'code': ['humaneval', 'context_length', 'input_price'],
'reasoning': ['mmlu', 'context_length', 'parameters'],
'vision': ['context_length', 'mmlu', 'input_price']
};
const FEATURE_LABELS = {
'context_length': '上下文',
'mmlu': 'MMLU',
'humaneval': 'HumanEval',
'input_price': '输入价',
'output_price': '输出价',
'parameters': '参数量',
'reasoning_capability': '推理',
'vision_capability': '视觉',
'multimodal': '多模态'
};
// 加载导航栏
async function loadNav() {
const res = await fetch('/api/categories');
categories = await res.json();
// 获取当前类别的子类别
currentCategory = categories.find(c => c.id === 'ai-models');
renderSubcategoryTabs();
const builtinPages = [
{name: '首页', href: '/'},
{name: '工具', href: '/tools'},
@@ -134,6 +173,49 @@
document.getElementById('topNav').innerHTML = navHtml;
}
// 渲染子类别选择器
function renderSubcategoryTabs() {
const container = document.getElementById('subcategoryTabs');
if (!currentCategory || !currentCategory.subcategories) {
container.innerHTML = '';
return;
}
let html = `<button onclick="selectSubcategory('')" class="px-4 py-2 ${currentSubcategory === '' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} rounded-lg text-sm" id="subcat-all">
<i class="ri-apps-line mr-1"></i>全部
</button>`;
currentCategory.subcategories.forEach(sub => {
const isActive = currentSubcategory === sub.id;
html += `<button onclick="selectSubcategory('${sub.id}')" class="px-4 py-2 ${isActive ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} rounded-lg text-sm" id="subcat-${sub.id}">
<i class="${sub.icon || 'ri-folder-line'} mr-1"></i>${sub.name}
</button>`;
});
container.innerHTML = html;
}
// 选择子类别
function selectSubcategory(subId) {
currentSubcategory = subId;
renderSubcategoryTabs();
loadModels();
}
// 获取当前子类别的关键特性
function getKeyFeatures() {
if (!currentSubcategory || !currentCategory || !currentCategory.subcategories) {
return ['parameters', 'context_length', 'mmlu', 'input_price'];
}
const subcat = currentCategory.subcategories.find(s => s.id === currentSubcategory);
if (subcat && subcat.key_features) {
return subcat.key_features;
}
return ['parameters', 'context_length', 'mmlu', 'input_price'];
}
async function loadModels() {
const keyword = document.getElementById('searchInput').value.trim();
@@ -155,6 +237,21 @@
models = models.filter(m => !m.is_open_source);
}
// 子类别过滤(通过模型名称/描述中的关键词判断)
if (currentSubcategory && currentCategory && currentCategory.subcategories) {
const subcat = currentCategory.subcategories.find(s => s.id === currentSubcategory);
if (subcat) {
// 简化过滤:根据子类别关键词匹配
// 实际应该有 subcategory_id 字段,这里暂时用名称匹配
// 用户可以在后台编辑时指定子类别
models = models.filter(m => {
const subcatField = m.subcategory || m.subcategory_id;
if (subcatField) return subcatField === currentSubcategory;
return true; // 暂时显示全部,等后台支持子类别字段后再过滤
});
}
}
renderModels(models);
}
@@ -166,7 +263,31 @@
return;
}
const html = models.map(m => `
// 动态获取关键特性
const keyFeatures = getKeyFeatures();
// 动态表头
let headerHtml = `
<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>
`;
keyFeatures.forEach(f => {
headerHtml += `<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">${FEATURE_LABELS[f] || f}</th>`;
});
headerHtml += `
<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>
`;
document.querySelector('#modelsTable thead').innerHTML = headerHtml;
// 动态内容
const html = models.map(m => {
let rowHtml = `
<tr class="border-b hover:bg-gray-50 transition ${m.is_pinned ? 'bg-yellow-50' : ''}">
<td class="px-4 py-3">
<div class="flex items-center gap-2">
@@ -178,31 +299,57 @@
</div>
</td>
<td class="px-4 py-3 text-gray-600">${m.organization}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">${m.parameters}B</span>
</td>
<td class="px-4 py-3 text-gray-600">${formatContext(m.context_length)}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">${m.mmlu || '-'}%</span>
</td>
`;
// 关键特性列
keyFeatures.forEach(f => {
const value = formatFeatureValue(f, m);
rowHtml += `<td class="px-4 py-3">${value}</td>`;
});
rowHtml += `
<td class="px-4 py-3">
${m.is_open_source
? '<span class="px-2 py-1 bg-emerald-100 text-emerald-700 rounded text-sm">开源</span>'
: '<span class="px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm">商业</span>'}
</td>
<td class="px-4 py-3 text-sm text-gray-600">
${m.input_price ? `$${m.input_price}/$${m.output_price}` : '免费'}
</td>
<td class="px-4 py-3 text-center">
<button onclick="showDetail('${m.id}')" class="text-indigo-600 hover:text-indigo-800 text-sm">
<i class="ri-eye-line mr-1"></i>详情
</button>
</td>
</tr>
`).join('');
`;
return rowHtml;
}).join('');
document.getElementById('modelsTable').innerHTML = html;
}
// 格式化特性值
function formatFeatureValue(feature, model) {
const value = model[feature];
if (value === null || value === undefined) return '<span class="text-gray-400">-</span>';
switch (feature) {
case 'parameters':
return `<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">${value}B</span>`;
case 'context_length':
return `<span class="text-gray-600">${formatContext(value)}</span>`;
case 'mmlu':
return `<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">${value}%</span>`;
case 'humaneval':
return `<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-sm">${value}%</span>`;
case 'input_price':
return `<span class="text-sm text-gray-600">$${value || 0}</span>`;
case 'output_price':
return `<span class="text-sm text-gray-600">$${value || 0}</span>`;
default:
return `<span class="text-gray-600">${value}</span>`;
}
}
function formatContext(len) {
if (!len) return '-';