已读完的书挪进「书籍」,抓取上限去掉(8 -> 整架 26 本)

抓取范围
- TOP_N 默认从 8 改成 0=不限制。原来那个 8 是我拍的「最近动过的前 8 本」,
  不是接口限制;整架 26 本也就 1+26 个请求,完全够用
- --top N 仍可用,想只抓最近几本时加

分组调整
- 「最近在读」只留 progress < 100 的(20 本),进度条不再出现 100%
- 新增「书籍」区底部的「微信读书 · 已读完」分组(6 本),卡片结构与手工
  书单一致,左上角带「已读完」徽标;因为微信读书的数据没有书评/简介,
  这一组点击直接跳 weread.qq.com,不弹窗
- 这一组带 data-weread-card + data-category="微信读书",所以搜索框和
  「领域」筛选同样作用得到;领域筛选里会多出一个「微信读书」选项
- 筛选逻辑把 [data-book-card] 的选择器扩成 [data-book-card],
  [data-weread-card],分组计数也一并算上

实测数据分布(整架 26 本)
- 已读完 6 / 在读中 16(1%-97%)/ 没开始 4(0%)

静态验证(dist/bookshelf/index.html)
- 在读区 20 张卡片,进度条 30/0/77/28/13/97/0/5/16/3/10/1/4/13/4/0/1/10/6/0,
  区内无「已读完」字样
- 已读完分组 6 张卡片,6 个徽标、6 个微信读书跳转链接,均带 data-weread-card
- 手工书单 3 张卡片未受影响;领域筛选新增「微信读书」按钮
This commit is contained in:
dsh
2026-09-15 10:45:29 +08:00
parent 95dd0d9552
commit 4df235ea21
5 changed files with 288 additions and 16 deletions
+69 -7
View File
@@ -5,10 +5,17 @@ import { cn } from "@/lib/utils"
import booksData from "../../public/data/books.json"
import { formatReadingDate, getReadingData } from "@/lib/reading"
// 微信读书「最近在读」。数据缺失时 reading 为 null,页面退化成空态,不报错。
// 微信读书数据。数据缺失时 reading 为 null,页面退化成空态,不报错。
const reading = getReadingData()
const readingBooks = reading?.books ?? []
const readingCount = readingBooks.length
// 读完的挪到「书籍」区,剩下的才留在「最近在读」
const inProgressReading = readingBooks.filter((book) => !book.finished)
const finishedReading = readingBooks.filter((book) => book.finished)
const readingCount = inProgressReading.length
/** 「书籍」区里给微信读书已读书记一个分类名,同时作为可筛选的领域 */
const WEREAD_CATEGORY = "微信读书"
// ---- Types
@@ -41,7 +48,7 @@ const booksByCategory = allBooks.reduce<Record<string, BookRecord[]>>((acc, book
return acc
}, {})
const bookCategories = ["全部", ...Object.keys(booksByCategory)]
const bookCategories = ["全部", ...Object.keys(booksByCategory), ...(finishedReading.length > 0 ? [WEREAD_CATEGORY] : [])]
const bookStatuses = ["全部", "推荐", "中庸", "不行"]
const bookStats = {
@@ -151,9 +158,9 @@ const recommendBadge = (status?: string) => {
) : null}
</div>
{readingBooks.length > 0 ? (
{inProgressReading.length > 0 ? (
<div class="grid gap-4 sm:grid-cols-2">
{readingBooks.map((book) => (
{inProgressReading.map((book) => (
<a
href={book.deepLink || undefined}
target={book.deepLink ? "_blank" : undefined}
@@ -352,6 +359,61 @@ const recommendBadge = (status?: string) => {
</div>
</section>
))}
{/* 微信读书里已读完的书,并入「书籍」区。
卡片结构和上面的书一样,但没有书评/简介,所以点击直接跳微信读书,不弹窗。
data-weread-card 让搜索框和领域/评价筛选同样作用于这一组。 */}
{finishedReading.length > 0 && (
<section data-books-section={WEREAD_CATEGORY}>
<div class="flex items-center gap-3 mb-5">
<h3 class="text-base font-semibold text-foreground">微信读书 · 已读完</h3>
<span class="text-xs text-muted-foreground" data-books-count>({finishedReading.length})</span>
<div class="flex-1 h-px bg-border/60" />
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5">
{finishedReading.map((book) => (
<a
href={book.deepLink || undefined}
target={book.deepLink ? "_blank" : undefined}
rel={book.deepLink ? "noopener noreferrer" : undefined}
data-weread-card
data-category={WEREAD_CATEGORY}
data-title={book.title.toLowerCase()}
data-authors={book.author.toLowerCase()}
class="group block cursor-pointer focus:outline-none"
>
<div class="relative aspect-[3/4] rounded-lg overflow-hidden mb-3 bg-surface-subtle shadow-sm group-hover:shadow-xl transition-shadow duration-300">
{book.cover ? (
<img
src={book.cover}
alt={book.title}
loading="lazy"
decoding="async"
referrerpolicy="no-referrer"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<div class="flex h-full w-full items-center justify-center p-3 bg-gradient-to-br from-primary/30 via-primary/20 to-primary/5">
<p class="font-semibold text-xs text-foreground text-center line-clamp-4">{book.title}</p>
</div>
)}
<span class="absolute top-2 right-2 text-[10px] px-2 py-0.5 rounded-full bg-status-positive-bg text-status-positive-fg font-medium">
已读完
</span>
</div>
<div class="min-h-[3.5rem] space-y-1">
<h4 class="font-medium text-sm text-foreground line-clamp-2 group-hover:text-primary transition-colors leading-snug">
{book.title}
</h4>
<p class="text-xs text-muted-foreground line-clamp-1">
{[book.author, book.category].filter(Boolean).join(" · ")}
</p>
</div>
</a>
))}
</div>
</section>
)}
</div>
{/* Empty state (shared, shown when filters return nothing) */}
@@ -466,7 +528,7 @@ const recommendBadge = (status?: string) => {
const applyBooks = () => {
const q = state.query.trim().toLowerCase();
const cards = document.querySelectorAll("[data-book-card]");
const cards = document.querySelectorAll("[data-book-card], [data-weread-card]");
let totalVisible = 0;
cards.forEach((card) => {
const cat = card.getAttribute("data-category") || "";
@@ -481,7 +543,7 @@ const recommendBadge = (status?: string) => {
if (show) totalVisible++;
});
document.querySelectorAll("[data-books-section]").forEach((section) => {
const visible = Array.from(section.querySelectorAll("[data-book-card]")).filter((c) => !c.classList.contains("hidden")).length;
const visible = Array.from(section.querySelectorAll("[data-book-card], [data-weread-card]")).filter((c) => !c.classList.contains("hidden")).length;
const countEl = section.querySelector("[data-books-count]");
if (countEl) countEl.textContent = `(${visible})`;
section.classList.toggle("hidden", visible === 0);