From 4c916eceb79ba9dd168c9402c620ebce6b4f1ed0 Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 17 Jul 2026 12:12:23 +0800 Subject: [PATCH] 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 --- app.py | 15 ++++-- static/css/style.css | 110 +++++++++++++++++++++++++++++++++++++++++++ static/js/app.js | 64 +++++++++++++++++++++++-- templates/index.html | 14 +++++- 4 files changed, 194 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index 60dd565..1caa29a 100644 --- a/app.py +++ b/app.py @@ -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") diff --git a/static/css/style.css b/static/css/style.css index 48b3850..e0a777b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; diff --git a/static/js/app.js b/static/js/app.js index bb73ef7..555e19d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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 = ``; + html += shelfCategories + .map((cat) => ``) + .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}`); diff --git a/templates/index.html b/templates/index.html index 0dedfea..4453921 100644 --- a/templates/index.html +++ b/templates/index.html @@ -51,6 +51,17 @@ + + +
@@ -110,13 +121,14 @@

📚 我的书架

- +
+