Files
param-hub-python/templates/category.html
hubian f2bb5dd2e8 feat: 动态分类页面改为表格展示方式
- 将卡片布局改为表格布局
- 动态生成表头(根据类别字段配置)
- 支持详情弹窗查看完整字段
- 保持搜索、排序、置顶等功能
- 与内置分类页面(models/gpus/cpus)展示风格统一
2026-04-29 16:43:11 +08:00

306 lines
13 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>
<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.css" rel="stylesheet">
</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">
<div class="flex items-center gap-2">
<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>
<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="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center gap-4">
<div class="w-16 h-16 rounded-xl bg-indigo-100 flex items-center justify-center">
<i class="{{ category.icon or 'ri-folder-line' }} text-3xl text-indigo-600"></i>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ category.name }}</h1>
<p class="text-gray-500 mt-1">{{ category.description or '参数数据' }}</p>
</div>
<div class="ml-auto">
<span class="text-sm text-gray-400"><span id="itemCount">0</span> 条数据</span>
</div>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="flex gap-4">
<div class="flex-1 relative">
<i class="ri-search-line absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="text" id="searchInput" placeholder="搜索..."
class="w-full pl-12 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:border-indigo-400"
oninput="filterItems()">
</div>
<select id="sortBy" onchange="loadItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
<option value="default">默认排序(置顶优先)</option>
<option value="publish_date">按发布日期</option>
<option value="views">按热度</option>
<option value="name">按名称</option>
<option value="created_at">按创建时间</option>
</select>
<select id="sortOrder" onchange="loadItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
<option value="desc">降序</option>
<option value="asc">升序</option>
</select>
</div>
</div>
<!-- 数据表格 -->
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b" id="tableHead">
<!-- 动态生成 -->
</thead>
<tbody id="itemsTable">
<tr><td colspan="10" class="text-center text-gray-400 py-8">加载中...</td></tr>
</tbody>
</table>
</div>
</main>
<!-- 详情弹窗 -->
<div id="detailModal" 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-[80vh] 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>
</div>
<!-- 页脚 -->
<footer class="bg-white border-t mt-8 py-6 text-center text-gray-500 text-sm">
ParamHub - 参数百科
</footer>
<script>
const categoryId = '{{ category.id }}';
let allItems = [];
let categories = [];
let currentCategory = null;
let displayFields = []; // 显示的字段列表
// 加载导航
async function loadNav() {
const res = await fetch('/api/categories');
categories = await res.json();
// 获取当前类别
currentCategory = categories.find(c => c.id === categoryId);
if (currentCategory && currentCategory.fields) {
// 过滤要显示的字段排除id、created_at等内部字段
displayFields = currentCategory.fields
.filter(f => !['id', 'category_id', 'created_at', 'updated_at', 'visible', 'is_pinned', 'views', 'publish_date', 'subcategory_id', 'parse_sources', 'product_images'].includes(f.key))
.slice(0, 6); // 最多显示6个字段
}
// 内置页面映射
const builtinPages = [
{id: 'home', name: '首页', href: '/'},
{id: 'tools', name: '工具', href: '/tools'},
{id: 'compare', name: '对比', href: '/compare'},
{id: 'knowledge', name: '知识库', href: '/knowledge'}
];
// 内置分类映射
const categoryPages = {
'ai-models': '/models',
'gpus': '/gpus',
'cpus': '/cpus'
};
let navHtml = '';
// 先添加内置页面
builtinPages.forEach(p => {
const isActive = 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 = cat.id === categoryId;
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;
}
// 加载数据
async function loadItems() {
const sortBy = document.getElementById('sortBy').value;
const sortOrder = document.getElementById('sortOrder').value;
const res = await fetch(`/api/items/${categoryId}?sort=${sortBy}&order=${sortOrder}`);
allItems = await res.json();
document.getElementById('itemCount').textContent = allItems.length;
renderTableHead();
renderItems(allItems);
}
// 渲染表格头部
function renderTableHead() {
let html = '<tr>';
// 名称列
html += '<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">名称</th>';
// 动态字段列
displayFields.forEach(f => {
html += `<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">${f.label}</th>`;
});
// 发布日期列
html += '<th class="px-4 py-3 text-left 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>';
html += '</tr>';
document.getElementById('tableHead').innerHTML = html;
}
// 渲染数据表格
function renderItems(items) {
if (items.length === 0) {
const colCount = displayFields.length + 3; // 名称 + 字段 + 日期 + 操作
document.getElementById('itemsTable').innerHTML = `
<tr><td colspan="${colCount}" class="text-center py-12">
<i class="ri-inbox-line text-4xl text-gray-300 mb-4 block"></i>
<p class="text-gray-400">暂无数据</p>
</td></tr>
`;
return;
}
const html = items.map(item => {
let row = `<tr class="border-b hover:bg-gray-50 transition ${item.is_pinned ? 'bg-yellow-50' : ''}">`;
// 名称列
row += `<td class="px-4 py-3">
<div class="font-medium text-gray-800 flex items-center gap-2">
${item.is_pinned ? '<i class="ri-pushpin-fill text-yellow-500" title="置顶"></i>' : ''}
${item.name || item.title || '未命名'}
</div>
${item.views ? `<div class="text-xs text-gray-400 mt-1"><i class="ri-eye-line mr-1"></i>${item.views}</div>` : ''}
</td>`;
// 动态字段列
displayFields.forEach(f => {
const value = item[f.key] || '-';
row += `<td class="px-4 py-3 text-gray-600 text-sm">${value}</td>`;
});
// 发布日期列
row += `<td class="px-4 py-3 text-gray-500 text-sm">${item.publish_date || (item.created_at ? item.created_at.split(' ')[0] : '-')}</td>`;
// 操作列
row += `<td class="px-4 py-3 text-center">
<button onclick="showDetail('${item.id}')" class="text-indigo-600 hover:text-indigo-800 text-sm">
<i class="ri-eye-line mr-1"></i>详情
</button>
</td>`;
row += '</tr>';
return row;
}).join('');
document.getElementById('itemsTable').innerHTML = html;
}
// 搜索过滤
function filterItems() {
const keyword = document.getElementById('searchInput').value.trim().toLowerCase();
if (!keyword) {
renderItems(allItems);
return;
}
const filtered = allItems.filter(item => {
return Object.values(item).some(val =>
String(val).toLowerCase().includes(keyword)
);
});
renderItems(filtered);
}
// 显示详情
function showDetail(id) {
const item = allItems.find(i => i.id === id);
if (!item) return;
document.getElementById('modalTitle').textContent = item.name || '详情';
let html = '<div class="space-y-3">';
// 按字段顺序显示
if (currentCategory && currentCategory.fields) {
currentCategory.fields.forEach(f => {
if (item[f.key]) {
const value = item[f.key];
html += `
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">${f.label}</span>
<span class="text-gray-800 ${f.input_style === 'long' ? 'text-right max-w-xs' : ''}">${value}</span>
</div>
`;
}
});
}
// 添加统计信息
if (item.views) {
html += `
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">热度</span>
<span class="text-gray-800"><i class="ri-eye-line mr-1"></i>${item.views}</span>
</div>
`;
}
html += '</div>';
document.getElementById('modalContent').innerHTML = html;
document.getElementById('detailModal').classList.remove('hidden');
}
// 关闭弹窗
function closeModal() {
document.getElementById('detailModal').classList.add('hidden');
}
// 点击弹窗外部关闭
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// 初始化
loadNav();
loadItems();
</script>
</body>
</html>