feat: 发布日期、热度、置顶、图片上传功能

- 新增发布日期(publish_date)、热度(views)、置顶(is_pinned)字段
- 后台管理表格显示新字段和置顶操作按钮
- 前端默认排序:置顶优先 → 发布日期最新
- 新增多种排序选项:发布日期、热度、名称等
- 新增图片上传API(支持多图上传)
- 后台管理表单添加图片上传组件(支持文件选择和粘贴)
- 数据创建时自动初始化新字段
This commit is contained in:
2026-04-20 21:25:57 +08:00
parent 627148a87f
commit 45190980a9
4 changed files with 634 additions and 90 deletions

View File

@@ -51,9 +51,16 @@
class="w-full pl-12 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:border-indigo-400"
onkeyup="filterItems()">
</div>
<select id="sortSelect" onchange="sortItems()" class="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none">
<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>
<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>
@@ -116,7 +123,9 @@
// 加载数据
async function loadItems() {
const res = await fetch(`/api/items/${categoryId}`);
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;
@@ -137,22 +146,28 @@
document.getElementById('itemsList').innerHTML = items.map(item => {
const fields = Object.entries(item)
.filter(([key, val]) => !['id', 'category_id', 'created_at', 'updated_at'].includes(key) && val)
.filter(([key, val]) => !['id', 'category_id', 'created_at', 'updated_at', 'visible', 'is_pinned', 'views', 'publish_date'].includes(key) && val)
.slice(0, 5)
.map(([key, val]) => `<span class="text-gray-500 text-sm">${key}: ${val}</span>`)
.join('<br>');
return `
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition group">
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition group ${item.is_pinned ? 'bg-yellow-50 border-yellow-300' : ''}">
<div class="flex items-start justify-between">
<div>
<h3 class="font-medium text-gray-800 group-hover:text-indigo-600">${item.name || item.title || '未命名'}</h3>
<h3 class="font-medium text-gray-800 group-hover:text-indigo-600 flex items-center gap-2">
${item.is_pinned ? '<i class="ri-pushpin-fill text-yellow-500" title="置顶"></i>' : ''}
${item.name || item.title || '未命名'}
</h3>
<div class="mt-2 space-y-1">
${fields}
</div>
</div>
<div class="text-xs text-gray-400">
${item.created_at ? item.created_at.split(' ')[0] : ''}
<div class="text-right">
<div class="text-xs text-gray-400">
${item.publish_date || (item.created_at ? item.created_at.split(' ')[0] : '')}
</div>
${item.views ? `<div class="text-xs text-gray-400 mt-1"><i class="ri-eye-line"></i> ${item.views}</div>` : ''}
</div>
</div>
</div>
@@ -176,19 +191,6 @@
renderItems(filtered);
}
// 排序
function sortItems() {
const sortBy = document.getElementById('sortSelect').value;
const sorted = [...allItems].sort((a, b) => {
if (sortBy === 'name') {
return (a.name || a.title || '').localeCompare(b.name || b.title || '');
} else {
return (b.created_at || '').localeCompare(a.created_at || '');
}
});
renderItems(sorted);
}
// 初始化
loadNav();
loadItems();