@@ -292,6 +292,9 @@ INDEX_TEMPLATE = '''
<!-- 列表 -->
<div id= " itemList " ></div>
<!-- 分页 -->
<div id= " pagination " class= " d-flex justify-content-center mt-3 " ></div>
</div>
</div>
</div>
@@ -483,10 +486,18 @@ INDEX_TEMPLATE = '''
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<div class= " mb-3 " >
<div class= " input-group " >
<input type= " text " id= " newTagName " class= " form-control " placeholder= " 新标签名称 " >
<button class= " btn btn-primary " onclick= " createTag() " ><i class=" bi bi-plus " ></i> 创建</button >
<div class= " row mb-3" >
<div class= " col " >
<div class= " input-group " >
<input type= " text " id= " tagSearch " class= " form-control " placeholder= " 搜索标签... " >
<button class= " btn btn-outline-secondary " onclick= " loadTagManagerList() " ><i class= " bi bi-search " ></i></button>
</div>
</div>
<div class= " col " >
<div class= " input-group " >
<input type= " text " id= " newTagName " class= " form-control " placeholder= " 新标签名称 " >
<button class= " btn btn-primary " onclick= " createTag() " ><i class= " bi bi-plus " ></i> 创建</button>
</div>
</div>
</div>
<div id= " tagListContainer " >
@@ -506,15 +517,18 @@ const API_BASE = '/api';
let currentFilter = { type: ' ' , status: ' ' };
// 初始化
document.addEventListener( ' DOMContentLoaded ' , () => {
document.addEventListener( ' DOMContentLoaded ' , async () => {
await loadStats(); // 先加载统计,确保总数可用
loadItems();
loadStats();
loadTags();
// 标签输入自动提示
document.getElementById( ' addTags ' ).addEventListener( ' input ' , showTagSuggestions);
document.getElementById( ' editTags ' ).addEventListener( ' input ' , showTagSuggestionsEdit);
// 标签搜索实时过滤
document.getElementById( ' tagSearch ' )?.addEventListener( ' input ' , debounce(loadTagManagerList, 300));
// 类型切换时显示/隐藏字段
document.getElementById( ' addType ' ).addEventListener( ' change ' , (e) => {
const type = e.target.value;
@@ -554,9 +568,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
// 加载列表
async function loadItems() {
let currentPage = 1;
const pageSize = 20;
async function loadItems(page = 1) {
currentPage = page;
const keyword = document.getElementById( ' searchInput ' ).value;
let url = `$ {API_BASE} /items?limit=100 `;
let url = `$ {API_BASE} /items?limit=$ {pageSize} &offset=$ { (page-1)*pageSize} `;
if (currentFilter.type) url += `&type=$ {currentFilter.type} `;
if (currentFilter.status) url += `&status=$ {currentFilter.status} `;
if (keyword) url += `&keyword=$ { encodeURIComponent(keyword)}`;
@@ -566,6 +584,7 @@ async function loadItems() {
if (data.success) {
renderItems(data.data);
renderPagination(data.data.length, page);
}
}
@@ -602,6 +621,53 @@ function renderItems(items) {
`).join( ' ' );
}
// 渲染分页
function renderPagination(itemCount, page) {
const container = document.getElementById( ' pagination ' );
const total = parseInt(document.getElementById( ' statTotal ' ).textContent);
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) {
container.innerHTML = ' ' ;
return;
}
let html = ' <nav><ul class= " pagination " > ' ;
// 上一页
html += `<li class= " page-item $ { page === 1 ? ' disabled ' : ' ' } " >
<a class= " page-link " href= " # " onclick= " loadItems($ { page-1}); return false; " >«</a>
</li>`;
// 页码( 最多显示5个)
const startPage = Math.max(1, page - 2);
const endPage = Math.min(totalPages, page + 2);
if (startPage > 1) {
html += `<li class= " page-item " ><a class= " page-link " href= " # " onclick= " loadItems(1); return false; " >1</a></li>`;
if (startPage > 2) html += `<li class= " page-item disabled " ><span class= " page-link " >...</span></li>`;
}
for (let p = startPage; p <= endPage; p++) {
html += `<li class= " page-item $ { p === page ? ' active ' : ' ' } " >
<a class= " page-link " href= " # " onclick= " loadItems($ {p} ); return false; " >$ {p} </a>
</li>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += `<li class= " page-item disabled " ><span class= " page-link " >...</span></li>`;
html += `<li class= " page-item " ><a class= " page-link " href= " # " onclick= " loadItems($ {totalPages} ); return false; " >$ {totalPages} </a></li>`;
}
// 下一页
html += `<li class= " page-item $ { page === totalPages ? ' disabled ' : ' ' } " >
<a class= " page-link " href= " # " onclick= " loadItems($ { page+1}); return false; " >»</a>
</li>`;
html += ' </ul></nav> ' ;
container.innerHTML = html;
}
// 加载统计
async function loadStats() {
const res = await fetch(`$ {API_BASE} /stats`);
@@ -614,6 +680,12 @@ async function loadStats() {
}
}
// 刷新数据(统计+列表)
async function refreshData() {
await loadStats();
loadItems(currentPage);
}
// 添加条目
async function addItem() {
const type = document.getElementById( ' addType ' ).value;
@@ -639,24 +711,21 @@ async function addItem() {
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById( ' addModal ' )).hide();
document.getElementById( ' addForm ' ).reset();
loadItems ();
loadStats();
refreshData ();
}
}
// 完成待办
async function completeItem(id) {
await fetch(`$ {API_BASE} /items/$ {id} /done`, { method: ' POST ' });
loadItems ();
loadStats();
refreshData ();
}
// 删除条目
async function deleteItem(id) {
if (!confirm( ' 确认删除? ' )) return;
await fetch(`$ {API_BASE} /items/$ {id} `, { method: ' DELETE ' });
loadItems ();
loadStats();
refreshData ();
}
// 当前查看的条目ID
@@ -782,8 +851,7 @@ async function saveEdit() {
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById( ' editModal ' )).hide();
loadItems ();
loadStats();
refreshData ();
}
}
@@ -907,12 +975,20 @@ async function loadTagManagerList() {
if (!data.success) return;
const container = document.getElementById( ' tagListContainer ' );
if (!data.data.length) {
// 搜索过滤
const searchKeyword = document.getElementById( ' tagSearch ' ).value.trim().toLowerCase();
let tags = data.data;
if (searchKeyword) {
tags = tags.filter(t => t.name.toLowerCase().includes(searchKeyword));
}
if (!tags.length) {
container.innerHTML = ' <div class= " text-center text-muted py-3 " >暂无标签</div> ' ;
return;
}
container.innerHTML = data.da ta.map(tag => `
container.innerHTML = tags .map(tag => `
<div class= " d-flex justify-content-between align-items-center p-2 border-bottom " >
<div>
<span class= " badge bg-secondary " >$ {tag.name} </span>