mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
feat: collapsible hierarchical category tags + category sort for shelf
- Backend: add parent_category param (matches '历史-*' sub-categories), sort=category mode (ORDER BY category ASC), return category_tree with parent→sub→count hierarchy - Frontend: replace flat 63-tag list with collapsible parent tags showing book counts, click parent to expand sub-category pills with fade-in animation, click sub to filter to exact category - Add '分类排序' toggle button: when active, books are grouped by category with colored section headers showing category name + count - Clicking same parent again collapses it; '全部' resets everything - Mobile responsive: smaller tag sizes, reduced indent on narrow screens
This commit is contained in:
1 parent
ec88707e6b
commit
e578a90543
4 files changed
+301
-30
No files matched your search
@@ -504,17 +504,21 @@ def api_shelf(conn):
|
|||||||
page (int): 页码,默认 1
|
page (int): 页码,默认 1
|
||||||
per_page (int): 每页数量,默认 60
|
per_page (int): 每页数量,默认 60
|
||||||
keyword (str): 书名或作者搜索(模糊匹配)
|
keyword (str): 书名或作者搜索(模糊匹配)
|
||||||
category (str): 分类筛选(精确匹配分类名)
|
category (str): 分类筛选(精确匹配完整分类名,如 "历史-中国古代")
|
||||||
|
parent_category (str): 父分类筛选(如 "历史" 匹配所有 "历史-*" 子分类)
|
||||||
finished (str): "1" 表示只看已读完
|
finished (str): "1" 表示只看已读完
|
||||||
|
sort (str): "category" 表示按分类排序,其他默认按置顶+阅读时间
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON: {books: [...], total, page, per_page, total_pages, categories: [...]}
|
JSON: {books: [...], total, page, per_page, total_pages, categories: [...], category_tree: {...}}
|
||||||
"""
|
"""
|
||||||
page = int(request.args.get("page", 1))
|
page = int(request.args.get("page", 1))
|
||||||
per_page = int(request.args.get("per_page", 60))
|
per_page = int(request.args.get("per_page", 60))
|
||||||
keyword = request.args.get("keyword", "").strip()
|
keyword = request.args.get("keyword", "").strip()
|
||||||
category = request.args.get("category", "").strip()
|
category = request.args.get("category", "").strip()
|
||||||
|
parent_category = request.args.get("parent_category", "").strip()
|
||||||
finished_only = request.args.get("finished", "0") == "1"
|
finished_only = request.args.get("finished", "0") == "1"
|
||||||
|
sort_mode = request.args.get("sort", "").strip()
|
||||||
|
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -528,6 +532,9 @@ def api_shelf(conn):
|
|||||||
if category:
|
if category:
|
||||||
where_clauses.append("category = %s")
|
where_clauses.append("category = %s")
|
||||||
params.append(category)
|
params.append(category)
|
||||||
|
if parent_category:
|
||||||
|
where_clauses.append("(category = %s OR category LIKE %s)")
|
||||||
|
params.extend([parent_category, f"{parent_category}-%"])
|
||||||
if finished_only:
|
if finished_only:
|
||||||
where_clauses.append("finish_reading=1")
|
where_clauses.append("finish_reading=1")
|
||||||
|
|
||||||
@@ -537,16 +544,40 @@ def api_shelf(conn):
|
|||||||
cursor.execute(f"SELECT COUNT(*) AS cnt FROM books{where_sql}", params)
|
cursor.execute(f"SELECT COUNT(*) AS cnt FROM books{where_sql}", params)
|
||||||
total = cursor.fetchone()["cnt"]
|
total = cursor.fetchone()["cnt"]
|
||||||
|
|
||||||
# 查询当前页数据(置顶优先,最近阅读时间倒序)
|
# 排序
|
||||||
|
if sort_mode == "category":
|
||||||
|
order_sql = " ORDER BY category ASC, is_top DESC, read_update_time DESC"
|
||||||
|
else:
|
||||||
|
order_sql = " ORDER BY is_top DESC, read_update_time DESC"
|
||||||
|
|
||||||
|
# 查询当前页数据
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"SELECT * FROM books{where_sql} ORDER BY is_top DESC, read_update_time DESC LIMIT %s OFFSET %s",
|
f"SELECT * FROM books{where_sql}{order_sql} LIMIT %s OFFSET %s",
|
||||||
params + [per_page, offset],
|
params + [per_page, offset],
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
# 获取所有分类列表(用于前端筛选下拉框)
|
# 获取所有分类列表及层级树
|
||||||
cursor.execute("SELECT DISTINCT category FROM books WHERE category != '' ORDER BY category")
|
cursor.execute(
|
||||||
categories = [r["category"] for r in cursor.fetchall()]
|
"SELECT category, COUNT(*) AS cnt FROM books "
|
||||||
|
"WHERE category IS NOT NULL AND category != '' "
|
||||||
|
"GROUP BY category ORDER BY category"
|
||||||
|
)
|
||||||
|
cat_rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# 构建层级树: { "历史": {"count": 114, "subs": {"世界史": 5, "中国古代": 10, ...}}, ... }
|
||||||
|
category_tree = {}
|
||||||
|
for r in cat_rows:
|
||||||
|
parts = r["category"].split("-", 1)
|
||||||
|
parent = parts[0]
|
||||||
|
sub = parts[1] if len(parts) > 1 else ""
|
||||||
|
if parent not in category_tree:
|
||||||
|
category_tree[parent] = {"count": 0, "subs": {}}
|
||||||
|
category_tree[parent]["count"] += r["cnt"]
|
||||||
|
if sub:
|
||||||
|
category_tree[parent]["subs"][sub] = r["cnt"]
|
||||||
|
|
||||||
|
categories = [r["category"] for r in cat_rows]
|
||||||
|
|
||||||
books = []
|
books = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -570,6 +601,7 @@ def api_shelf(conn):
|
|||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"total_pages": (total + per_page - 1) // per_page,
|
"total_pages": (total + per_page - 1) // per_page,
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
|
"category_tree": category_tree,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+143
-5
@@ -336,14 +336,119 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 书架分类标签 ===== */
|
/* ===== 书架分类标签 ===== */
|
||||||
|
/* 层级折叠分类标签 */
|
||||||
.shelf-categories {
|
.shelf-categories {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-category-tag {
|
.shelf-cat-parent {
|
||||||
|
padding: 5px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-card);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-parent:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-parent.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
background: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-count {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-parent.active .shelf-cat-count {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-arrow {
|
||||||
|
font-size: 9px;
|
||||||
|
margin-left: 2px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-subs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 0 4px 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-sub {
|
||||||
|
padding: 3px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-light);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-sub:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-sub.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
background: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-count-sm {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--accent-bg);
|
||||||
|
color: var(--accent-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-cat-sub.active .shelf-cat-count-sm {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类排序按钮 */
|
||||||
|
.shelf-sort-btn {
|
||||||
padding: 5px 14px;
|
padding: 5px 14px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -355,19 +460,45 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-category-tag:hover {
|
.shelf-sort-btn:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--accent-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-category-tag.active {
|
.shelf-sort-btn.active {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分类分组标题 */
|
||||||
|
.shelf-group-header {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 24px 0 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--accent-bg);
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-group-header:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-group-count {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-group-header + .book-grid {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Hero 区 ===== */
|
/* ===== Hero 区 ===== */
|
||||||
.hero-section {
|
.hero-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1906,10 +2037,17 @@ body {
|
|||||||
.shelf-categories {
|
.shelf-categories {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.shelf-category-tag {
|
.shelf-cat-parent {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
}
|
}
|
||||||
|
.shelf-cat-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
}
|
||||||
|
.shelf-cat-subs {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
.back-to-top {
|
.back-to-top {
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
|||||||
+118
-18
@@ -222,15 +222,18 @@ function globalSearch() {
|
|||||||
doSearch();
|
doSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 书架分类标签 =====
|
// ===== 书架分类标签(可折叠层级结构) =====
|
||||||
|
|
||||||
let shelfCategories = [];
|
let shelfCategoryTree = {}; // { "历史": {count:114, subs:{"世界史":5,...}}, ... }
|
||||||
let shelfActiveCategory = ""; // 空字符串表示"全部"
|
let shelfActiveCategory = ""; // 完整分类名,如 "历史-中国古代"
|
||||||
|
let shelfActiveParent = ""; // 父分类名,如 "历史";空表示"全部"
|
||||||
|
let shelfExpandedParents = new Set(); // 当前展开的父分类
|
||||||
|
let shelfSortMode = "default"; // "default" 或 "category"
|
||||||
|
|
||||||
async function loadShelfCategories() {
|
async function loadShelfCategories() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchAPI("/api/shelf?page=1&per_page=1");
|
const data = await fetchAPI("/api/shelf?page=1&per_page=1");
|
||||||
shelfCategories = data.categories || [];
|
shelfCategoryTree = data.category_tree || {};
|
||||||
renderShelfCategories();
|
renderShelfCategories();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("加载分类失败:", e);
|
console.error("加载分类失败:", e);
|
||||||
@@ -241,19 +244,87 @@ function renderShelfCategories() {
|
|||||||
const container = document.getElementById("shelfCategories");
|
const container = document.getElementById("shelfCategories");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let html = `<button class="shelf-category-tag ${shelfActiveCategory === "" ? "active" : ""}" onclick="filterByCategory('')">全部</button>`;
|
let html = "";
|
||||||
html += shelfCategories
|
|
||||||
.map((cat) => `<button class="shelf-category-tag ${shelfActiveCategory === cat ? "active" : ""}" onclick="filterByCategory('${cat.replace(/'/g, "\\'")}')">${cat}</button>`)
|
// "全部" 按钮
|
||||||
.join("");
|
const allActive = !shelfActiveParent && !shelfActiveCategory;
|
||||||
|
html += `<button class="shelf-cat-parent ${allActive ? "active" : ""}" onclick="filterByParent('')">全部</button>`;
|
||||||
|
|
||||||
|
// 父分类按钮(按书籍数量降序)
|
||||||
|
const parents = Object.entries(shelfCategoryTree)
|
||||||
|
.sort((a, b) => b[1].count - a[1].count);
|
||||||
|
|
||||||
|
for (const [parent, info] of parents) {
|
||||||
|
const isActive = shelfActiveParent === parent && !shelfActiveCategory;
|
||||||
|
const isExpanded = shelfExpandedParents.has(parent);
|
||||||
|
const arrow = Object.keys(info.subs).length > 0
|
||||||
|
? `<span class="shelf-cat-arrow ${isExpanded ? "expanded" : ""}">${isExpanded ? "▼" : "▶"}</span>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
html += `<button class="shelf-cat-parent ${isActive ? "active" : ""}"
|
||||||
|
onclick="filterByParent('${parent.replace(/'/g, "\\'")}')">${parent}
|
||||||
|
<span class="shelf-cat-count">${info.count}</span>${arrow}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
// 展开的子分类
|
||||||
|
if (isExpanded && Object.keys(info.subs).length > 0) {
|
||||||
|
html += `<div class="shelf-cat-subs">`;
|
||||||
|
for (const [sub, count] of Object.entries(info.subs).sort((a, b) => b[1] - a[1])) {
|
||||||
|
const fullCat = `${parent}-${sub}`;
|
||||||
|
const subActive = shelfActiveCategory === fullCat;
|
||||||
|
html += `<button class="shelf-cat-sub ${subActive ? "active" : ""}"
|
||||||
|
onclick="event.stopPropagation();filterByCategory('${fullCat.replace(/'/g, "\\'")}')">${sub}
|
||||||
|
<span class="shelf-cat-count-sm">${count}</span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByCategory(category) {
|
function filterByParent(parent) {
|
||||||
shelfActiveCategory = category;
|
if (parent) {
|
||||||
|
// 点击父分类:展开/折叠子分类,同时筛选该父分类下的所有书
|
||||||
|
if (shelfExpandedParents.has(parent) && shelfActiveParent === parent && !shelfActiveCategory) {
|
||||||
|
// 已展开且已选中 → 折叠
|
||||||
|
shelfExpandedParents.delete(parent);
|
||||||
|
shelfActiveParent = "";
|
||||||
|
shelfActiveCategory = "";
|
||||||
|
} else {
|
||||||
|
// 展开 + 筛选
|
||||||
|
shelfExpandedParents.add(parent);
|
||||||
|
shelfActiveParent = parent;
|
||||||
|
shelfActiveCategory = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 点击"全部"
|
||||||
|
shelfExpandedParents.clear();
|
||||||
|
shelfActiveParent = "";
|
||||||
|
shelfActiveCategory = "";
|
||||||
|
}
|
||||||
renderShelfCategories();
|
renderShelfCategories();
|
||||||
loadShelf(1);
|
loadShelf(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterByCategory(fullCategory) {
|
||||||
|
shelfActiveCategory = fullCategory;
|
||||||
|
// 确保父分类已展开
|
||||||
|
const parent = fullCategory.split("-")[0];
|
||||||
|
shelfExpandedParents.add(parent);
|
||||||
|
shelfActiveParent = parent;
|
||||||
|
renderShelfCategories();
|
||||||
|
loadShelf(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShelfSort() {
|
||||||
|
shelfSortMode = shelfSortMode === "category" ? "default" : "category";
|
||||||
|
const btn = document.getElementById("shelfSortBtn");
|
||||||
|
btn.classList.toggle("active", shelfSortMode === "category");
|
||||||
|
loadShelf(1);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 初始化 =====
|
// ===== 初始化 =====
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
@@ -612,7 +683,12 @@ async function loadShelf(page) {
|
|||||||
const finished = document.getElementById("finishedOnly").checked ? "1" : "0";
|
const finished = document.getElementById("finishedOnly").checked ? "1" : "0";
|
||||||
const params = new URLSearchParams({ page: page, per_page: 60, finished: finished });
|
const params = new URLSearchParams({ page: page, per_page: 60, finished: finished });
|
||||||
if (keyword) params.set("keyword", keyword);
|
if (keyword) params.set("keyword", keyword);
|
||||||
if (shelfActiveCategory) params.set("category", shelfActiveCategory);
|
if (shelfActiveCategory) {
|
||||||
|
params.set("category", shelfActiveCategory);
|
||||||
|
} else if (shelfActiveParent) {
|
||||||
|
params.set("parent_category", shelfActiveParent);
|
||||||
|
}
|
||||||
|
if (shelfSortMode === "category") params.set("sort", "category");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchAPI(`/api/shelf?${params}`);
|
const data = await fetchAPI(`/api/shelf?${params}`);
|
||||||
@@ -621,8 +697,22 @@ async function loadShelf(page) {
|
|||||||
document.getElementById("shelfInfo").textContent =
|
document.getElementById("shelfInfo").textContent =
|
||||||
`共 ${data.total} 本 · 第 ${page}/${data.total_pages} 页`;
|
`共 ${data.total} 本 · 第 ${page}/${data.total_pages} 页`;
|
||||||
|
|
||||||
|
if (shelfSortMode === "category" && data.books.length > 0) {
|
||||||
|
// 按分类分组渲染
|
||||||
|
grid.innerHTML = renderBooksGroupedByCategory(data.books);
|
||||||
|
} else {
|
||||||
grid.innerHTML = data.books
|
grid.innerHTML = data.books
|
||||||
.map((book) => {
|
.map((book) => renderBookCard(book))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPagination(page, data.total_pages);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载书架失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBookCard(book) {
|
||||||
const badges = [];
|
const badges = [];
|
||||||
if (book.is_top) badges.push(`<span class="book-top-badge">置顶</span>`);
|
if (book.is_top) badges.push(`<span class="book-top-badge">置顶</span>`);
|
||||||
if (book.finish_reading) badges.push(`<span class="book-finished-badge">已读完</span>`);
|
if (book.finish_reading) badges.push(`<span class="book-finished-badge">已读完</span>`);
|
||||||
@@ -637,13 +727,23 @@ async function loadShelf(page) {
|
|||||||
<div class="book-title">${book.title}</div>
|
<div class="book-title">${book.title}</div>
|
||||||
<div class="book-author">${book.author}</div>
|
<div class="book-author">${book.author}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
renderPagination(page, data.total_pages);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("加载书架失败:", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBooksGroupedByCategory(books) {
|
||||||
|
// 按 category 分组
|
||||||
|
const groups = {};
|
||||||
|
books.forEach((book) => {
|
||||||
|
const cat = book.category || "未分类";
|
||||||
|
if (!groups[cat]) groups[cat] = [];
|
||||||
|
groups[cat].push(book);
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
for (const [cat, items] of Object.entries(groups)) {
|
||||||
|
html += `<div class="shelf-group-header">${cat} <span class="shelf-group-count">${items.length} 本</span></div>`;
|
||||||
|
html += `<div class="book-grid">${items.map(renderBookCard).join("")}</div>`;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPagination(current, total) {
|
function renderPagination(current, total) {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
<label class="filter-checkbox">
|
<label class="filter-checkbox">
|
||||||
<input type="checkbox" id="finishedOnly"> 仅看已读完
|
<input type="checkbox" id="finishedOnly"> 仅看已读完
|
||||||
</label>
|
</label>
|
||||||
|
<button class="shelf-sort-btn" id="shelfSortBtn" onclick="toggleShelfSort()">分类排序</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="shelf-info" id="shelfInfo"></div>
|
<div class="shelf-info" id="shelfInfo"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user