feat: add global search bar, fix search cover blur, shelf title search + category tags

1. Fix hdCover() to also replace s_ prefix (search results use s_ not t6_)
2. Add persistent search bar below navbar, switches to discover tab on search
3. Shelf search now supports book title and author search (new keyword param)
4. Add category tag pills above book grid, click to filter by category
5. Update /api/shelf backend: keyword param for title/author, category for exact match
6. Mobile responsive styles for search bar and category tags
This commit is contained in:
admin committed 2026-07-17 12:12:23 +08:00
1 parent 7c918140b3
commit 4c916eceb7
4 files changed
+194 -9

No files matched your search

+10 -5
View File
@@ -498,12 +498,13 @@ def api_time_distribution(conn):
def api_shelf(conn):
"""
书架列表接口(分页)。
返回书架中的书籍列表,支持按分类筛选和只看已读完。
返回书架中的书籍列表,支持按书名/作者搜索、按分类筛选和只看已读完。
Query Parameters:
page (int): 页码,默认 1
per_page (int): 每页数量,默认 60
category (str): 分类筛选(模糊匹配)
keyword (str): 书名或作者搜索(模糊匹配)
category (str): 分类筛选(精确匹配分类名)
finished (str): "1" 表示只看已读完
Returns:
@@ -511,7 +512,8 @@ def api_shelf(conn):
"""
page = int(request.args.get("page", 1))
per_page = int(request.args.get("per_page", 60))
category = request.args.get("category", "")
keyword = request.args.get("keyword", "").strip()
category = request.args.get("category", "").strip()
finished_only = request.args.get("finished", "0") == "1"
offset = (page - 1) * per_page
@@ -520,9 +522,12 @@ def api_shelf(conn):
# 构建查询条件
where_clauses = []
params = []
if keyword:
where_clauses.append("(title LIKE %s OR author LIKE %s)")
params.extend([f"%{keyword}%", f"%{keyword}%"])
if category:
where_clauses.append("category LIKE %s")
params.append(f"%{category}%")
where_clauses.append("category = %s")
params.append(category)
if finished_only:
where_clauses.append("finish_reading=1")
+110
View File
@@ -271,6 +271,103 @@ body {
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle { color: var(--toggle-icon-sun); }
/* ===== 顶部常驻搜索框 ===== */
.global-search-bar {
position: sticky;
top: 60px;
z-index: 90;
background: var(--nav-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 10px 0;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.global-search-wrapper {
display: flex;
align-items: center;
gap: 0;
max-width: 600px;
margin: 0 auto;
position: relative;
}
.global-search-icon {
position: absolute;
left: 14px;
font-size: 15px;
opacity: 0.5;
pointer-events: none;
}
.global-search-input {
flex: 1;
padding: 9px 16px 9px 40px;
border: 1px solid var(--border);
border-right: none;
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
font-size: 14px;
background: var(--bg-card);
color: var(--text-primary);
transition: border-color 0.2s, box-shadow 0.2s;
}
.global-search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-bg);
}
.global-search-btn {
padding: 9px 20px;
border: 1px solid var(--accent);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
background: var(--accent);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.global-search-btn:hover {
background: var(--accent-dark);
}
/* ===== 书架分类标签 ===== */
.shelf-categories {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.shelf-category-tag {
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;
}
.shelf-category-tag:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-bg);
}
.shelf-category-tag.active {
border-color: var(--accent);
color: #fff;
background: var(--accent);
font-weight: 500;
}
/* ===== Hero 区 ===== */
.hero-section {
position: relative;
@@ -1730,6 +1827,19 @@ body {
.nav-update {
display: none;
}
.global-search-wrapper {
max-width: 100%;
}
.shelf-search {
width: 140px;
}
.shelf-categories {
gap: 6px;
}
.shelf-category-tag {
font-size: 12px;
padding: 4px 10px;
}
.back-to-top {
bottom: 20px;
right: 20px;
+61 -3
View File
@@ -34,9 +34,17 @@ function getChartColors() {
// ===== 工具函数 =====
/**
* 升级微信读书封面图 URL 到高清晰度版本。
* 微信读书 CDN 的封面图通过文件名前缀区分分辨率:
* t6_ = 低清缩略图(~150px,Retina 屏模糊)
* s_ = 超低清缩略图(~90px,搜索结果常用,更模糊)
* t8_ = 高清版(~500px,适合 2x/3x 屏幕)
* 将 t6_ 和 s_ 前缀统一升级为 t8_ 高清版。
*/
function hdCover(url) {
if (!url) return "";
return url.replace("/t6_", "/t8_");
return url.replace(/\/(t6_|s_)/, "/t8_");
}
async function fetchAPI(path) {
@@ -147,6 +155,53 @@ function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
// ===== 全局搜索(顶部常驻搜索框) =====
function globalSearch() {
const keyword = document.getElementById("globalSearchInput").value.trim();
if (!keyword) return;
// 切换到推荐标签页
switchTab("discover");
// 切换到搜索子标签
switchDiscoverTab("search");
// 填入搜索词并执行搜索
document.getElementById("searchInput").value = keyword;
doSearch();
}
// ===== 书架分类标签 =====
let shelfCategories = [];
let shelfActiveCategory = ""; // 空字符串表示"全部"
async function loadShelfCategories() {
try {
const data = await fetchAPI("/api/shelf?page=1&per_page=1");
shelfCategories = data.categories || [];
renderShelfCategories();
} catch (e) {
console.error("加载分类失败:", e);
}
}
function renderShelfCategories() {
const container = document.getElementById("shelfCategories");
if (!container) return;
let html = `<button class="shelf-category-tag ${shelfActiveCategory === "" ? "active" : ""}" onclick="filterByCategory('')">全部</button>`;
html += shelfCategories
.map((cat) => `<button class="shelf-category-tag ${shelfActiveCategory === cat ? "active" : ""}" onclick="filterByCategory('${cat.replace(/'/g, "\\'")}')">${cat}</button>`)
.join("");
container.innerHTML = html;
}
function filterByCategory(category) {
shelfActiveCategory = category;
renderShelfCategories();
loadShelf(1);
}
// ===== 初始化 =====
document.addEventListener("DOMContentLoaded", async () => {
@@ -155,6 +210,7 @@ document.addEventListener("DOMContentLoaded", async () => {
loadOverview();
loadTopBooks();
loadShelfCategories();
loadShelf(1);
loadNotebooks();
loadRecommend(0);
@@ -500,9 +556,11 @@ let shelfCurrentPage = 1;
async function loadShelf(page) {
shelfCurrentPage = page;
const search = document.getElementById("shelfSearch").value;
const keyword = document.getElementById("shelfSearch").value.trim();
const finished = document.getElementById("finishedOnly").checked ? "1" : "0";
const params = new URLSearchParams({ page: page, per_page: 60, category: search, finished: finished });
const params = new URLSearchParams({ page: page, per_page: 60, finished: finished });
if (keyword) params.set("keyword", keyword);
if (shelfActiveCategory) params.set("category", shelfActiveCategory);
try {
const data = await fetchAPI(`/api/shelf?${params}`);
+13 -1
View File
@@ -51,6 +51,17 @@
</div>
</nav>
<!-- 顶部常驻搜索框 -->
<div class="global-search-bar">
<div class="container">
<div class="global-search-wrapper">
<span class="global-search-icon">🔍</span>
<input type="text" id="globalSearchInput" class="global-search-input" placeholder="搜索图书..." onkeydown="if(event.key==='Enter')globalSearch()">
<button class="global-search-btn" onclick="globalSearch()">搜索</button>
</div>
</div>
</div>
<!-- ===== 主视图(标签页切换) ===== -->
<main id="mainView">
@@ -110,13 +121,14 @@
<h2 class="section-title">📚 我的书架</h2>
<div class="shelf-controls">
<div class="shelf-filters">
<input type="text" id="shelfSearch" class="shelf-search" placeholder="搜索分类...">
<input type="text" id="shelfSearch" class="shelf-search" placeholder="搜索书名、作者...">
<label class="filter-checkbox">
<input type="checkbox" id="finishedOnly"> 仅看已读完
</label>
</div>
<div class="shelf-info" id="shelfInfo"></div>
</div>
<div class="shelf-categories" id="shelfCategories"></div>
<div class="book-grid" id="bookGrid"></div>
<div class="pagination" id="pagination"></div>
</div>