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:
admin committed 2026-07-17 12:21:36 +08:00
1 parent ec88707e6b
commit e578a90543
4 files changed
+312 -41

No files matched your search

+39 -7
View File
@@ -504,17 +504,21 @@ def api_shelf(conn):
page (int): 页码,默认 1
per_page (int): 每页数量,默认 60
keyword (str): 书名或作者搜索(模糊匹配)
category (str): 分类筛选(精确匹配分类名)
category (str): 分类筛选(精确匹配完整分类名,如 "历史-中国古代")
parent_category (str): 父分类筛选(如 "历史" 匹配所有 "历史-*" 子分类)
finished (str): "1" 表示只看已读完
sort (str): "category" 表示按分类排序,其他默认按置顶+阅读时间
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))
per_page = int(request.args.get("per_page", 60))
keyword = request.args.get("keyword", "").strip()
category = request.args.get("category", "").strip()
parent_category = request.args.get("parent_category", "").strip()
finished_only = request.args.get("finished", "0") == "1"
sort_mode = request.args.get("sort", "").strip()
offset = (page - 1) * per_page
cursor = conn.cursor()
@@ -528,6 +532,9 @@ def api_shelf(conn):
if category:
where_clauses.append("category = %s")
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:
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)
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(
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],
)
rows = cursor.fetchall()
# 获取所有分类列表(用于前端筛选下拉框)
cursor.execute("SELECT DISTINCT category FROM books WHERE category != '' ORDER BY category")
categories = [r["category"] for r in cursor.fetchall()]
# 获取所有分类列表及层级树
cursor.execute(
"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 = []
for row in rows:
@@ -570,6 +601,7 @@ def api_shelf(conn):
"per_page": per_page,
"total_pages": (total + per_page - 1) // per_page,
"categories": categories,
"category_tree": category_tree,
})