428 lines
18 KiB
HTML
428 lines
18 KiB
HTML
<!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>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
|
<style>
|
|
.compare-card { transition: all 0.2s; }
|
|
.compare-card:hover { transform: translateY(-2px); }
|
|
.compare-card.selected { border-color: #4f46e5; background: #f5f3ff; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<!-- 导航栏 -->
|
|
<nav class="bg-white shadow-sm sticky top-0 z-50">
|
|
<div class="max-w-7xl mx-auto px-4 py-3 flex justify-between items-center">
|
|
<a href="/" class="flex items-center gap-2">
|
|
<i class="ri-dashboard-3-line text-2xl text-indigo-600"></i>
|
|
<span class="text-xl font-bold text-gray-800">ParamHub</span>
|
|
</a>
|
|
<div class="flex gap-4 text-sm" id="topNav"></div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="max-w-7xl mx-auto px-4 py-8">
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
|
<i class="ri-git-merge-line text-purple-600"></i>
|
|
对比工具
|
|
</h1>
|
|
<p class="text-gray-500 mt-1">选择类别,对比最多5个产品的参数</p>
|
|
</div>
|
|
|
|
<!-- 类别选择 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
|
|
<label class="text-sm font-medium text-gray-600 mb-3 block">选择对比类别</label>
|
|
<div id="categoryButtons" class="flex flex-wrap gap-2">
|
|
<span class="text-gray-400">加载中...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 已选择的产品 -->
|
|
<div id="selectedArea" class="bg-white rounded-xl shadow-sm p-4 mb-6 hidden">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<label class="text-sm font-medium text-gray-600">
|
|
已选择 <span id="selectedCount">0</span>/5 个产品
|
|
</label>
|
|
<button onclick="clearSelection()" class="text-sm text-red-600 hover:text-red-700">
|
|
<i class="ri-close-line mr-1"></i>清空选择
|
|
</button>
|
|
</div>
|
|
<div id="selectedProducts" class="flex flex-wrap gap-2"></div>
|
|
</div>
|
|
|
|
<!-- 产品列表 -->
|
|
<div id="productListArea" class="mb-6 hidden">
|
|
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
|
|
<div class="flex justify-between items-center">
|
|
<label class="text-sm font-medium text-gray-600">
|
|
点击选择要对比的产品(最多5个)
|
|
</label>
|
|
<input type="text" id="searchInput" placeholder="搜索产品..."
|
|
class="px-3 py-1 border rounded-lg text-sm w-48"
|
|
oninput="filterProducts()">
|
|
</div>
|
|
</div>
|
|
<div id="productList" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3"></div>
|
|
</div>
|
|
|
|
<!-- 对比结果 -->
|
|
<div id="compareResult" class="bg-white rounded-xl shadow-sm p-6 hidden">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
|
<i class="ri-bar-chart-line mr-2 text-purple-600"></i>对比结果
|
|
</h2>
|
|
<div id="compareTable" class="overflow-x-auto"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let categories = [];
|
|
let currentCategory = null;
|
|
let categoryData = {};
|
|
let selectedIds = [];
|
|
const MAX_COMPARE = 5;
|
|
|
|
// 加载导航栏
|
|
async function loadNav() {
|
|
const res = await fetch('/api/categories');
|
|
categories = await res.json();
|
|
|
|
const builtinPages = [
|
|
{name: '首页', href: '/'},
|
|
{name: '工具', href: '/tools'},
|
|
{name: '对比', href: '/compare'},
|
|
{name: '知识库', href: '/knowledge'}
|
|
];
|
|
|
|
const categoryPages = {
|
|
'ai-models': '/models',
|
|
'gpus': '/gpus',
|
|
'cpus': '/cpus'
|
|
};
|
|
|
|
let navHtml = '';
|
|
builtinPages.forEach(p => {
|
|
const isActive = window.location.pathname === p.href;
|
|
navHtml += `<a href="${p.href}" class="${isActive ? 'text-indigo-600 font-medium' : 'text-gray-600 hover:text-indigo-600'}">${p.name}</a>`;
|
|
});
|
|
|
|
categories.forEach(cat => {
|
|
const href = categoryPages[cat.id] || `/category/${cat.id}`;
|
|
const isActive = window.location.pathname === href;
|
|
navHtml += `<a href="${href}" class="${isActive ? 'text-indigo-600 font-medium' : 'text-gray-600 hover:text-indigo-600'}">${cat.name}</a>`;
|
|
});
|
|
|
|
document.getElementById('topNav').innerHTML = navHtml;
|
|
}
|
|
|
|
// 渲染类别按钮
|
|
function renderCategoryButtons() {
|
|
const colorMap = {
|
|
blue: 'bg-blue-100 text-blue-700 hover:bg-blue-200',
|
|
green: 'bg-green-100 text-green-700 hover:bg-green-200',
|
|
purple: 'bg-purple-100 text-purple-700 hover:bg-purple-200',
|
|
orange: 'bg-orange-100 text-orange-700 hover:bg-orange-200',
|
|
teal: 'bg-teal-100 text-teal-700 hover:bg-teal-200',
|
|
red: 'bg-red-100 text-red-700 hover:bg-red-200',
|
|
indigo: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200'
|
|
};
|
|
|
|
const html = categories.map(cat => {
|
|
const colorClass = colorMap[cat.color] || 'bg-gray-100 text-gray-700 hover:bg-gray-200';
|
|
const activeClass = currentCategory?.id === cat.id ? 'ring-2 ring-indigo-500 ring-offset-2' : '';
|
|
return `<button onclick="selectCategory('${cat.id}')"
|
|
class="px-4 py-2 rounded-lg ${colorClass} ${activeClass} transition">
|
|
<i class="${cat.icon} mr-1"></i>${cat.name}
|
|
</button>`;
|
|
}).join('');
|
|
|
|
document.getElementById('categoryButtons').innerHTML = html;
|
|
}
|
|
|
|
// 选择类别
|
|
async function selectCategory(categoryId) {
|
|
currentCategory = categories.find(c => c.id === categoryId);
|
|
if (!currentCategory) return;
|
|
|
|
// 更新按钮状态
|
|
renderCategoryButtons();
|
|
|
|
// 清空之前的选择
|
|
selectedIds = [];
|
|
updateSelectedArea();
|
|
|
|
// 加载该类别数据
|
|
const endpoint = getEndpoint(categoryId);
|
|
const res = await fetch(endpoint);
|
|
categoryData[categoryId] = await res.json();
|
|
|
|
// 显示产品列表
|
|
document.getElementById('productListArea').classList.remove('hidden');
|
|
renderProductList();
|
|
document.getElementById('compareResult').classList.add('hidden');
|
|
}
|
|
|
|
// 获取API端点
|
|
function getEndpoint(categoryId) {
|
|
const builtinMap = {
|
|
'ai-models': '/api/models',
|
|
'gpus': '/api/gpus',
|
|
'cpus': '/api/cpus'
|
|
};
|
|
return builtinMap[categoryId] || `/api/items/${categoryId}`;
|
|
}
|
|
|
|
// 渲染产品列表
|
|
function renderProductList() {
|
|
if (!currentCategory) return;
|
|
|
|
const products = categoryData[currentCategory.id] || [];
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
|
|
const filtered = products.filter(p =>
|
|
(p.name || '').toLowerCase().includes(searchTerm) ||
|
|
(p.organization || p.manufacturer || p.brand || '').toLowerCase().includes(searchTerm)
|
|
);
|
|
|
|
const colorMap = {
|
|
blue: 'bg-blue-50 border-blue-200',
|
|
green: 'bg-green-50 border-green-200',
|
|
purple: 'bg-purple-50 border-purple-200',
|
|
orange: 'bg-orange-50 border-orange-200',
|
|
teal: 'bg-teal-50 border-teal-200'
|
|
};
|
|
const bgColor = colorMap[currentCategory.color] || 'bg-gray-50 border-gray-200';
|
|
|
|
if (filtered.length === 0) {
|
|
document.getElementById('productList').innerHTML = `
|
|
<div class="col-span-full text-center text-gray-400 py-8">暂无数据</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const html = filtered.map(p => {
|
|
const isSelected = selectedIds.includes(p.id);
|
|
const selectedClass = isSelected ? 'ring-2 ring-indigo-500 bg-indigo-50' : '';
|
|
return `
|
|
<div onclick="toggleProduct('${p.id}')"
|
|
class="compare-card p-3 rounded-lg border cursor-pointer ${bgColor} ${selectedClass}">
|
|
<div class="font-medium text-gray-800">${p.name || '-'}</div>
|
|
<div class="text-sm text-gray-500">${p.organization || p.manufacturer || p.brand || ''}</div>
|
|
${isSelected ? '<div class="mt-2 text-xs text-indigo-600"><i class="ri-check-line mr-1"></i>已选择</div>' : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('productList').innerHTML = html;
|
|
}
|
|
|
|
// 切换产品选择
|
|
function toggleProduct(productId) {
|
|
const idx = selectedIds.indexOf(productId);
|
|
if (idx >= 0) {
|
|
selectedIds.splice(idx, 1);
|
|
} else if (selectedIds.length < MAX_COMPARE) {
|
|
selectedIds.push(productId);
|
|
} else {
|
|
alert(`最多只能对比${MAX_COMPARE}个产品`);
|
|
return;
|
|
}
|
|
|
|
updateSelectedArea();
|
|
renderProductList();
|
|
|
|
if (selectedIds.length >= 2) {
|
|
showCompareResult();
|
|
} else {
|
|
document.getElementById('compareResult').classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 更新已选择区域
|
|
function updateSelectedArea() {
|
|
const area = document.getElementById('selectedArea');
|
|
const container = document.getElementById('selectedProducts');
|
|
const count = document.getElementById('selectedCount');
|
|
|
|
if (selectedIds.length === 0) {
|
|
area.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
area.classList.remove('hidden');
|
|
count.textContent = selectedIds.length;
|
|
|
|
const products = categoryData[currentCategory.id] || [];
|
|
const html = selectedIds.map(id => {
|
|
const p = products.find(x => x.id === id);
|
|
if (!p) return '';
|
|
return `
|
|
<div class="px-3 py-1 bg-indigo-100 text-indigo-700 rounded-full text-sm flex items-center gap-2">
|
|
<span>${p.name}</span>
|
|
<button onclick="event.stopPropagation(); toggleProduct('${id}')" class="hover:text-indigo-900">
|
|
<i class="ri-close-line"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// 清空选择
|
|
function clearSelection() {
|
|
selectedIds = [];
|
|
updateSelectedArea();
|
|
renderProductList();
|
|
document.getElementById('compareResult').classList.add('hidden');
|
|
}
|
|
|
|
// 搜索过滤
|
|
function filterProducts() {
|
|
renderProductList();
|
|
}
|
|
|
|
// 显示对比结果
|
|
async function showCompareResult() {
|
|
if (selectedIds.length < 2) return;
|
|
|
|
const products = categoryData[currentCategory.id] || [];
|
|
const selected = selectedIds.map(id => products.find(p => p.id === id)).filter(Boolean);
|
|
|
|
// 获取对比字段
|
|
const fields = getCompareFields();
|
|
|
|
// 生成对比表格
|
|
let html = `
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600 sticky left-0 bg-gray-50">参数</th>
|
|
${selected.map(p => `<th class="px-4 py-3 text-center text-sm font-medium text-gray-800 min-w-[150px]">${p.name}</th>`).join('')}
|
|
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600 min-w-[100px]">差异范围</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y">
|
|
`;
|
|
|
|
fields.forEach(f => {
|
|
const values = selected.map(p => p[f.key]);
|
|
const hasNumber = values.some(v => typeof v === 'number' && v !== null);
|
|
|
|
let minVal = null, maxVal = null;
|
|
if (hasNumber) {
|
|
const nums = values.filter(v => typeof v === 'number' && v !== null);
|
|
if (nums.length > 0) {
|
|
minVal = Math.min(...nums);
|
|
maxVal = Math.max(...nums);
|
|
}
|
|
}
|
|
|
|
html += `<tr class="hover:bg-gray-50">
|
|
<td class="px-4 py-3 text-gray-600 sticky left-0 bg-white">${f.label}</td>`;
|
|
|
|
values.forEach(v => {
|
|
let display = '-';
|
|
if (v !== null && v !== undefined && v !== '') {
|
|
if (f.type === 'boolean') {
|
|
display = v ? '<span class="text-green-600">是</span>' : '<span class="text-gray-400">否</span>';
|
|
} else if (f.type === 'number') {
|
|
display = v;
|
|
} else if (f.type === 'json' && typeof v === 'object') {
|
|
display = `<span class="text-xs text-gray-500">${JSON.stringify(v).substring(0, 30)}...</span>`;
|
|
} else {
|
|
display = String(v);
|
|
}
|
|
}
|
|
html += `<td class="px-4 py-3 text-center font-medium">${display}</td>`;
|
|
});
|
|
|
|
// 差异范围
|
|
let diffRange = '-';
|
|
if (minVal !== null && maxVal !== null && minVal !== maxVal) {
|
|
diffRange = `<span class="text-orange-600">${minVal} ~ ${maxVal}</span>`;
|
|
if (f.unit) diffRange += `<span class="text-gray-400 text-xs ml-1">${f.unit}</span>`;
|
|
} else if (minVal !== null) {
|
|
diffRange = '<span class="text-gray-400">相同</span>';
|
|
}
|
|
|
|
html += `<td class="px-4 py-3 text-center text-sm">${diffRange}</td>`;
|
|
html += '</tr>';
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
document.getElementById('compareTable').innerHTML = html;
|
|
document.getElementById('compareResult').classList.remove('hidden');
|
|
}
|
|
|
|
// 获取对比字段
|
|
function getCompareFields() {
|
|
if (!currentCategory) return [];
|
|
|
|
// 使用类别的 fields 配置
|
|
const categoryFields = currentCategory.fields || [];
|
|
|
|
if (categoryFields.length > 0) {
|
|
return categoryFields
|
|
.filter(f => !['id', 'created_at', 'updated_at', 'visible', 'raw_text', 'images', 'is_pinned'].includes(f.key))
|
|
.slice(0, 15) // 最多显示15个字段
|
|
.map(f => ({
|
|
key: f.key,
|
|
label: f.label,
|
|
type: f.type,
|
|
unit: f.unit || null
|
|
}));
|
|
}
|
|
|
|
// 兼容旧的内置类别(无 fields 配置时)
|
|
const builtinFields = {
|
|
'ai-models': [
|
|
{ key: 'name', label: '名称', type: 'text' },
|
|
{ key: 'organization', label: '厂商', type: 'text' },
|
|
{ key: 'parameters', label: '参数量', type: 'number', unit: 'B' },
|
|
{ key: 'context_length', label: '上下文长度', type: 'number' },
|
|
{ key: 'mmlu', label: 'MMLU', type: 'number', unit: '%' },
|
|
{ key: 'humaneval', label: 'HumanEval', type: 'number', unit: '%' },
|
|
{ key: 'is_open_source', label: '开源', type: 'boolean' },
|
|
{ key: 'input_price', label: '输入价格', type: 'number', unit: '$/1K' },
|
|
{ key: 'output_price', label: '输出价格', type: 'number', unit: '$/1K' },
|
|
],
|
|
'gpus': [
|
|
{ key: 'name', label: '名称', type: 'text' },
|
|
{ key: 'manufacturer', label: '厂商', type: 'text' },
|
|
{ key: 'architecture', label: '架构', type: 'text' },
|
|
{ key: 'memory_gb', label: '显存', type: 'number', unit: 'GB' },
|
|
{ key: 'cuda_cores', label: 'CUDA核心', type: 'number' },
|
|
{ key: 'fp16_tflops', label: 'FP16性能', type: 'number', unit: 'TF' },
|
|
{ key: 'price_usd', label: '价格', type: 'number', unit: '$' },
|
|
],
|
|
'cpus': [
|
|
{ key: 'name', label: '名称', type: 'text' },
|
|
{ key: 'manufacturer', label: '厂商', type: 'text' },
|
|
{ key: 'cores', label: '核心数', type: 'number' },
|
|
{ key: 'threads', label: '线程数', type: 'number' },
|
|
{ key: 'base_clock_ghz', label: '基础频率', type: 'number', unit: 'GHz' },
|
|
{ key: 'boost_clock_ghz', label: '加速频率', type: 'number', unit: 'GHz' },
|
|
{ key: 'l3_cache_mb', label: 'L3缓存', type: 'number', unit: 'MB' },
|
|
{ key: 'tdp_watts', label: 'TDP', type: 'number', unit: 'W' },
|
|
{ key: 'price_usd', label: '价格', type: 'number', unit: '$' },
|
|
]
|
|
};
|
|
|
|
return builtinFields[currentCategory.id] || [];
|
|
}
|
|
|
|
// 初始化
|
|
async function init() {
|
|
await loadNav();
|
|
renderCategoryButtons();
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |