From 8a867132deb78002c5f6177ba163c1a531061e74 Mon Sep 17 00:00:00 2001 From: dsh Date: Tue, 15 Sep 2026 10:51:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E8=AF=BB=E5=AE=8C=E7=9A=84=E4=B9=A6?= =?UTF-8?q?=E5=8D=A1=E6=94=B9=E6=88=90=E5=92=8C=E3=80=8C=E5=9C=A8=E8=AF=BB?= =?UTF-8?q?=E3=80=8D=E5=90=8C=E4=B8=80=E5=A5=97=E6=A0=B7=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E5=BC=80=E5=A7=8B/=E8=AF=BB=E5=AE=8C=E6=97=B6?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 可行性(先说结论) - 「开始阅读时间」在官方文档里没列,但 /book/getprogress 的原始回包里有 book.startReadingTime,6 本已读完的全都有 - 顺带纠正我上一轮的错误结论:真正常为 0 的是 recordReadingTime, book.readingTime 是有值的(西线无战事 6190 秒 = 103 分), 已存进 JSON 的 readingSeconds,卡片暂未展示 抽公共组件 - 新增 src/components/reading-card.astro,在读和已读完共用,样式强制统一: 横版卡片、封面在左、信息在右 - 两处只在底部两行不同: 在读 进度条 + 百分比 / 最近阅读 X 已读完 开始 X / 读完 Y(无进度条) 数据层 - fetch-reading.mjs 每个条目新增 startedAt / finishedAt / readingSeconds - src/lib/reading.ts 的 ReadingBook 类型同步补上(都可选,老数据不会炸) - startReadingTime / finishTime 缺失时卡片自动少一行,不会渲染出「开始 —」 时间显示走 Asia/Shanghai。所以西线无战事的 startReadingTime 1786207913(UTC 08-08T15:31Z)在页面上是「开始 2026-08-09」, 这才是本地日期。 静态验证(dist/bookshelf/index.html) - 在读区 20 张卡 / 20 条进度条 / 20 个「最近阅读」 - 已读完区 6 张卡 / 0 条进度条 / 6 个「开始 X」/ 6 个「读完 X」 - 抽一张:西线无战事(译文经典) 雷马克 · 文学-外国文学 开始 2026-08-09 读完 2026-09-06,确认不含进度条 - 搜索/领域筛选的 data-weread-card 仍挂在新卡片上,筛选不受影响 --- README.md | 12 ++- scripts/fetch-reading.mjs | 18 ++++- src/components/bookshelf-content.astro | 107 ++++--------------------- src/components/reading-card.astro | 81 +++++++++++++++++++ src/data/reading.json | 80 +++++++++++++++++- src/lib/reading.ts | 8 +- 6 files changed, 208 insertions(+), 98 deletions(-) create mode 100644 src/components/reading-card.astro diff --git a/README.md b/README.md index b3875f6..a314cfd 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,8 @@ src/components/bookshelf-content.astro - 请求体里 `skill_version` **必填**,业务参数**平铺在顶层**,不能包进 `params` - `/shelf/sync` 一次就给全封面/书名/作者/分类/最近阅读时间/跳转链接,**只有阅读进度**要额外调 `/book/getprogress`,所以请求数 = 1 + N - `book.progress` 是 0–100 的整数,**只有 100 才算读完** -- `book.recordReadingTime` **恒为 0**,所以卡片不做「累计时长」 +- `book.recordReadingTime` **恒为 0**(别用它),但 `book.readingTime` 是真数据,累计时长取后者(已存进 JSON 的 `readingSeconds`,卡片暂未展示) +- `book.startReadingTime` **官方文档没列,但回包里确实有**,就是「开始阅读时间」;只有 readUpdateTime 那 26 本里有 8 本没有它(基本都是没怎么打开的书) - `deepLink` 实测是普通 https(`https://weread.qq.com/book-detail?...`),浏览器能直接打开 - `category` 是 `文学-外国文学` 这种「大类-子类」复合串 - 时间戳统一按 `YYYY-MM-DD` 展示(官方硬性要求),顺带避开「N 天前」在静态站上会随构建时间变旧 @@ -309,7 +310,14 @@ src/components/bookshelf-content.astro | 最近在读 | `progress < 100`(含 0%,标「还没开始」) | | 书籍 → 「微信读书 · 已读完」分组 | `progress === 100` | -这一组混在「书籍」区的网格里,卡片结构和手工书单一致,但没有书评/简介,所以点击是直接跳微信读书而不是弹窗。它们带 `data-weread-card` 和 `data-category="微信读书"`,所以搜索框和「领域」筛选同样作用得到(领域里会多出一个「微信读书」选项)。过滤掉的:私密阅读(`secret === 1`)、从没打开过的书;有声书不处理。 +**两处用的是同一个组件 `src/components/reading-card.astro`**,保证样式统一 —— 横版卡片、封面在左、信息在右,只有底部两行不同: + +| | 第 1 行 | 第 2 行 | +|---|---|---| +| 在读 | 进度条 + 百分比 | 最近阅读 X | +| 已读完 | 开始 X | 读完 Y | + +「已读完」那一组混在「书籍」区里,但没有书评/简介,所以点击直接跳微信读书而不是弹窗。它们带 `data-weread-card` 和 `data-category="微信读书"`,所以搜索框和「领域」筛选同样作用得到(领域里会多出一个「微信读书」选项)。过滤掉的:私密阅读(`secret === 1`)、从没打开过的书;有声书不处理。 **2. 书籍** —— 来自 `public/data/books.json`,由 `bookshelf-content.astro` 在构建时 import(文件必须存在)。另外两个文件 `papers.json` / `comics.json` 对应的漫画、论文分类已经移除,文件保留但不再被引用。 diff --git a/scripts/fetch-reading.mjs b/scripts/fetch-reading.mjs index 0914ef1..788e129 100644 --- a/scripts/fetch-reading.mjs +++ b/scripts/fetch-reading.mjs @@ -15,7 +15,8 @@ * 两处实测得到的结论,别被直觉带偏: * 1. /shelf/sync 一次就给全了封面/书名/作者/分类/最近阅读时间/跳转链接, * 只有「阅读进度」要额外调 /book/getprogress,所以请求数 = 1 + N - * 2. book.recordReadingTime 恒为 0,拿不到累计时长,所以卡片不做这一项 + * 2. book.recordReadingTime 恒为 0,但 book.readingTime 是真数据,累计时长取后者 + * 3. 官方文档没列 book.startReadingTime,但回包里有,是「开始阅读时间」 */ import fs from "node:fs" @@ -104,6 +105,12 @@ async function fetchReading() { progress: null, finished: false, lastReadAt: new Date(Number(book.readUpdateTime) * 1000).toISOString(), + // 下面三个来自 /book/getprogress。官方文档没列 startReadingTime, + // 但原始回包里确实有(读完的 6 本全都有),是「开始阅读时间」。 + startedAt: null, + finishedAt: null, + /** 累计阅读秒数。注意是 readingTime,不是恒为 0 的 recordReadingTime */ + readingSeconds: null, deepLink: book.deepLink ?? "", } @@ -115,6 +122,15 @@ async function fetchReading() { // progress 是 0-100 的整数,只有正好 100 才算读完 entry.finished = reading.progress === 100 } + if (reading.startReadingTime) { + entry.startedAt = new Date(reading.startReadingTime * 1000).toISOString() + } + if (reading.finishTime) { + entry.finishedAt = new Date(reading.finishTime * 1000).toISOString() + } + if (typeof reading.readingTime === "number") { + entry.readingSeconds = reading.readingTime + } } catch (error) { // 单本节失败不该让整次抓取挂掉,卡片退化成「不显示进度条」 console.warn(` ⚠️ ${book.title}:进度获取失败 — ${error.message}`) diff --git a/src/components/bookshelf-content.astro b/src/components/bookshelf-content.astro index 07bf98e..3355c63 100644 --- a/src/components/bookshelf-content.astro +++ b/src/components/bookshelf-content.astro @@ -3,6 +3,7 @@ import SiteHeader from "@/components/site-header.astro" import Footer from "@/components/footer.astro" import { cn } from "@/lib/utils" import booksData from "../../public/data/books.json" +import ReadingCard from "@/components/reading-card.astro" import { formatReadingDate, getReadingData } from "@/lib/reading" // 微信读书数据。数据缺失时 reading 为 null,页面退化成空态,不报错。 @@ -160,59 +161,7 @@ const recommendBadge = (status?: string) => { {inProgressReading.length > 0 ? (
- {inProgressReading.map((book) => ( - -
- {book.cover ? ( - {book.title} - ) : ( -
-

{book.title}

-
- )} -
- -
) : (
@@ -361,7 +310,7 @@ const recommendBadge = (status?: string) => { ))} {/* 微信读书里已读完的书,并入「书籍」区。 - 卡片结构和上面的书一样,但没有书评/简介,所以点击直接跳微信读书,不弹窗。 + 卡片用和「最近在读」同一个 ReadingCard 组件,只是底部换成「开始/读完」两行、没有进度条。 data-weread-card 让搜索框和领域/评价筛选同样作用于这一组。 */} {finishedReading.length > 0 && (
@@ -370,48 +319,20 @@ const recommendBadge = (status?: string) => { ({finishedReading.length})
-
)}
diff --git a/src/components/reading-card.astro b/src/components/reading-card.astro new file mode 100644 index 0000000..5ad94f9 --- /dev/null +++ b/src/components/reading-card.astro @@ -0,0 +1,81 @@ +--- +import { formatReadingDate, type ReadingBook } from "@/lib/reading" + +/** + * 微信读书书卡。在读和已读完共用同一个组件,保证样式统一 —— + * 两者只在底部那两行不同: + * 在读 → 进度条 + 百分比 / 「最近阅读 X」 + * 已读完 → 「开始 X」/「读完 Y」(没有进度条) + */ +interface Props { + book: ReadingBook + /** 额外挂在 上的属性,比如给搜索/筛选用的 data-* */ + attrs?: Record +} + +const { book, attrs = {} } = Astro.props + +const startedText = formatReadingDate(book.startedAt) +const finishedText = formatReadingDate(book.finishedAt) +--- + + +
+ {book.cover ? ( + {book.title} + ) : ( +
+

{book.title}

+
+ )} +
+ +
+

+ {book.title} +

+

+ {[book.author, book.category].filter(Boolean).join(" · ")} +

+ +
+ {book.finished ? ( + <> + {startedText && ( +

开始 {startedText}

+ )} +

+ 读完 {finishedText || "—"} +

+ + ) : ( + <> +
+
+
+
+ + {!book.progress ? "还没开始" : `${book.progress}%`} + +
+

+ 最近阅读 {formatReadingDate(book.lastReadAt)} +

+ + )} +
+
+
diff --git a/src/data/reading.json b/src/data/reading.json index bf92071..b90dca1 100644 --- a/src/data/reading.json +++ b/src/data/reading.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-09-15T02:45:18.862Z", + "updatedAt": "2026-09-15T02:51:38.693Z", "source": "weread", "books": [ { @@ -11,6 +11,9 @@ "progress": 30, "finished": false, "lastReadAt": "2026-09-13T15:08:26.000Z", + "startedAt": "2026-09-04T16:07:07.000Z", + "finishedAt": null, + "readingSeconds": 4030, "deepLink": "https://weread.qq.com/book-detail?type=1&v=34c329807197141b34cf1db" }, { @@ -22,6 +25,9 @@ "progress": 0, "finished": false, "lastReadAt": "2026-09-09T05:05:04.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 20, "deepLink": "https://weread.qq.com/book-detail?type=1&v=bbc32a50813ab7953g01522a" }, { @@ -33,6 +39,9 @@ "progress": 77, "finished": false, "lastReadAt": "2026-09-07T09:27:28.000Z", + "startedAt": "2026-06-03T15:44:54.000Z", + "finishedAt": "2026-06-09T11:02:09.000Z", + "readingSeconds": 2021, "deepLink": "https://weread.qq.com/book-detail?type=1&v=de6321e0813abab86g01378d" }, { @@ -44,6 +53,9 @@ "progress": 100, "finished": true, "lastReadAt": "2026-09-06T09:45:42.000Z", + "startedAt": "2026-08-08T16:51:53.000Z", + "finishedAt": "2026-09-06T05:26:12.000Z", + "readingSeconds": 6190, "deepLink": "https://weread.qq.com/book-detail?type=1&v=a1932750813ab6e79g017e78" }, { @@ -55,6 +67,9 @@ "progress": 28, "finished": false, "lastReadAt": "2026-09-06T05:26:38.000Z", + "startedAt": "2026-08-05T16:49:22.000Z", + "finishedAt": null, + "readingSeconds": 7639, "deepLink": "https://weread.qq.com/book-detail?type=1&v=80a324f0716b1a6480af682" }, { @@ -66,6 +81,9 @@ "progress": 13, "finished": false, "lastReadAt": "2026-06-15T16:24:59.000Z", + "startedAt": "2026-06-05T15:43:45.000Z", + "finishedAt": null, + "readingSeconds": 589, "deepLink": "https://weread.qq.com/book-detail?type=1&v=b4432330813abb059g01728a" }, { @@ -77,6 +95,9 @@ "progress": 97, "finished": false, "lastReadAt": "2026-06-10T16:14:02.000Z", + "startedAt": "2026-04-17T15:59:58.000Z", + "finishedAt": null, + "readingSeconds": 14309, "deepLink": "https://weread.qq.com/book-detail?type=1&v=140329d0716ce81f140468e" }, { @@ -88,6 +109,9 @@ "progress": 0, "finished": false, "lastReadAt": "2026-06-10T16:13:58.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 89, "deepLink": "https://weread.qq.com/book-detail?type=1&v=32f329e0813aba1d3g018fee" }, { @@ -99,6 +123,9 @@ "progress": 100, "finished": true, "lastReadAt": "2026-06-07T10:50:15.000Z", + "startedAt": "2026-05-24T13:35:18.000Z", + "finishedAt": "2026-06-06T16:01:25.000Z", + "readingSeconds": 7391, "deepLink": "https://weread.qq.com/book-detail?type=1&v=fb8320d0813ab7b86g01401c" }, { @@ -110,6 +137,9 @@ "progress": 5, "finished": false, "lastReadAt": "2026-06-07T05:15:01.000Z", + "startedAt": "2026-05-06T23:27:18.000Z", + "finishedAt": null, + "readingSeconds": 303, "deepLink": "https://weread.qq.com/book-detail?type=1&v=ec0325c0813ab718cg017a62" }, { @@ -121,6 +151,9 @@ "progress": 16, "finished": false, "lastReadAt": "2026-06-06T18:14:57.000Z", + "startedAt": "2026-05-02T08:55:25.000Z", + "finishedAt": null, + "readingSeconds": 1591, "deepLink": "https://weread.qq.com/book-detail?type=1&v=ec332c80813abb519g019b32" }, { @@ -132,6 +165,9 @@ "progress": 3, "finished": false, "lastReadAt": "2026-06-01T17:12:29.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 106, "deepLink": "https://weread.qq.com/book-detail?type=1&v=da032ef072517ae3da039f2" }, { @@ -143,6 +179,9 @@ "progress": 10, "finished": false, "lastReadAt": "2026-05-13T00:11:25.000Z", + "startedAt": "2025-01-07T10:42:41.000Z", + "finishedAt": null, + "readingSeconds": 1321, "deepLink": "https://weread.qq.com/book-detail?type=1&v=a3d32d20719debdba3d5de1" }, { @@ -154,6 +193,9 @@ "progress": 1, "finished": false, "lastReadAt": "2026-04-19T15:00:03.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 291, "deepLink": "https://weread.qq.com/book-detail?type=1&v=7c632ef05a49197c62b53f0" }, { @@ -165,6 +207,9 @@ "progress": 4, "finished": false, "lastReadAt": "2026-04-18T15:44:35.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 160, "deepLink": "https://weread.qq.com/book-detail?type=1&v=93d325a0719b200493d5ba9" }, { @@ -176,6 +221,9 @@ "progress": 13, "finished": false, "lastReadAt": "2026-03-28T10:03:33.000Z", + "startedAt": "2024-12-21T00:52:19.000Z", + "finishedAt": null, + "readingSeconds": 2916, "deepLink": "https://weread.qq.com/book-detail?type=1&v=2a332d10726332f72a34943" }, { @@ -187,6 +235,9 @@ "progress": 100, "finished": true, "lastReadAt": "2026-03-28T10:01:44.000Z", + "startedAt": "2026-03-20T14:42:26.000Z", + "finishedAt": "2026-03-22T16:49:18.000Z", + "readingSeconds": 15371, "deepLink": "https://weread.qq.com/book-detail?type=1&v=6d232640727324446d2e978" }, { @@ -198,6 +249,9 @@ "progress": 100, "finished": true, "lastReadAt": "2026-03-25T17:32:16.000Z", + "startedAt": "2026-03-23T09:59:40.000Z", + "finishedAt": "2026-03-25T17:30:10.000Z", + "readingSeconds": 4246, "deepLink": "https://weread.qq.com/book-detail?type=1&v=8033247071e0e1808035ab9" }, { @@ -209,6 +263,9 @@ "progress": 4, "finished": false, "lastReadAt": "2026-01-11T10:35:21.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 196, "deepLink": "https://weread.qq.com/book-detail?type=1&v=b0b32630813abaadeg0157d1" }, { @@ -220,6 +277,9 @@ "progress": 0, "finished": false, "lastReadAt": "2025-10-29T00:43:47.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 272, "deepLink": "https://weread.qq.com/book-detail?type=1&v=5d332c2072575dbf5d33fe2" }, { @@ -231,6 +291,9 @@ "progress": 100, "finished": true, "lastReadAt": "2025-10-29T00:43:35.000Z", + "startedAt": "2019-07-19T04:20:51.000Z", + "finishedAt": "2019-07-24T05:23:50.000Z", + "readingSeconds": 7718, "deepLink": "https://weread.qq.com/book-detail?type=1&v=4e3321805b01d74e3fa7481" }, { @@ -242,6 +305,9 @@ "progress": 1, "finished": false, "lastReadAt": "2025-10-18T09:21:26.000Z", + "startedAt": "2025-03-26T09:48:02.000Z", + "finishedAt": null, + "readingSeconds": 611, "deepLink": "https://weread.qq.com/book-detail?type=1&v=2bb32ff0813ab6ffcg014315" }, { @@ -253,6 +319,9 @@ "progress": 10, "finished": false, "lastReadAt": "2024-12-21T00:51:54.000Z", + "startedAt": "2024-12-17T04:42:59.000Z", + "finishedAt": null, + "readingSeconds": 549, "deepLink": "https://weread.qq.com/book-detail?type=1&v=2c032e80813ab95aag019524" }, { @@ -264,6 +333,9 @@ "progress": 6, "finished": false, "lastReadAt": "2020-04-09T07:29:49.000Z", + "startedAt": "2020-04-09T07:23:34.000Z", + "finishedAt": null, + "readingSeconds": 728, "deepLink": "https://weread.qq.com/book-detail?type=1&v=814322405a4585814beb03d" }, { @@ -275,6 +347,9 @@ "progress": 100, "finished": true, "lastReadAt": "2019-10-15T15:30:42.000Z", + "startedAt": "2019-08-07T02:20:48.000Z", + "finishedAt": "2019-08-07T07:41:59.000Z", + "readingSeconds": 7494, "deepLink": "https://weread.qq.com/book-detail?type=1&v=770328405c5160770c987ae" }, { @@ -286,6 +361,9 @@ "progress": 0, "finished": false, "lastReadAt": "2017-05-30T00:49:56.000Z", + "startedAt": null, + "finishedAt": null, + "readingSeconds": 137, "deepLink": "https://weread.qq.com/book-detail?type=1&v=47532ac05c659b47554a825" } ] diff --git a/src/lib/reading.ts b/src/lib/reading.ts index 1f632c6..6ab9e26 100644 --- a/src/lib/reading.ts +++ b/src/lib/reading.ts @@ -20,8 +20,14 @@ export type ReadingBook = { /** 0-100 的整数;null 表示这次没取到进度(单本失败) */ progress: number | null finished: boolean - /** ISO 字符串 */ + /** 最近一次阅读时间,ISO 字符串 */ lastReadAt: string + /** 开始阅读时间,ISO;部分书没有这个字段 */ + startedAt?: string | null + /** 读完时间,ISO;只有读完的书才有 */ + finishedAt?: string | null + /** 累计阅读秒数 */ + readingSeconds?: number | null /** 微信读书的跳转链接,实测是普通 https,浏览器能直接打开 */ deepLink: string }