Compare commits
7
Commits
219cb712a6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff698be696 | ||
|
|
4512d3fb24 | ||
|
|
ddfd0c0402 | ||
|
|
940bac3106 | ||
|
|
2673e236f7 | ||
|
|
daf3b34b8c | ||
|
|
ec80ea7bd0 |
@@ -48,6 +48,7 @@ pnpm dev
|
||||
| `pnpm build` | 构建到 `dist/`,并跑 Pagefind 建立搜索索引 |
|
||||
| `pnpm preview` | 本地预览构建产物 |
|
||||
| `pnpm check` | Astro 的类型与内容校验(本项目没有单元测试框架,这就是唯一的自动检查) |
|
||||
| `pnpm verify:css` | 单独跑构建产物 CSS 自检(`pnpm build` 和 `deploy.sh` 里都会自动跑) |
|
||||
| `pnpm new-thought "..."` | 快速创建一条「偶得」(见下) |
|
||||
|
||||
## 项目结构
|
||||
@@ -97,6 +98,27 @@ onPage("my-widget", () => {
|
||||
页面级的小交互如果**不需要 JS** 就别写 JS:阅读进度条和回到顶部按钮的显隐都由
|
||||
CSS 滚动驱动动画(`animation-timeline: scroll()`)负责,脚本只管点击与老浏览器降级。
|
||||
|
||||
### ⚠️ 滚动驱动动画的两条硬规矩
|
||||
|
||||
2026-09-21 踩过一次:线上按钮不显示、本地 dev 一切正常。原因是 Lightning CSS
|
||||
(Tailwind v4 的压缩器)把 `animation-timeline` 并进了 `animation` 简写,产出
|
||||
`animation: linear both back-to-top-reveal scroll(root)` —— 而 WebKit 不认简写里的
|
||||
timeline 分量([webkit#293276](https://wiki.webkit.org/show_bug.cgi?id=293276)),
|
||||
**整条声明被判无效丢弃**,元素永远停在初始的隐藏样式;更坑的是
|
||||
`CSS.supports("animation-timeline: scroll()")` 照样返回 true,运行时特性检测看不出问题
|
||||
([lightningcss#815](https://github.com/parcel-bundler/lightningcss/issues/815))。因此:
|
||||
|
||||
1. **写 CSS 时**:`animation-timeline` / `animation-range` 那条规则的选择器要和
|
||||
`animation` 简写那条**不同**(现有代码用 `.reading-progress > .reading-progress-bar`
|
||||
和 `.back-to-top[data-back-to-top]`),否则压缩器会把两条相邻的同选择器规则合并。
|
||||
刻意用后代 / 属性选择器而不是 `:where()`,是为了让特异性不低于简写那条 ——
|
||||
`animation` 简写会把 `animation-timeline` 重置回 `auto`。
|
||||
2. **写 JS 时**:不要用 `CSS.supports()` 判断滚动动画是否可用,直接读
|
||||
`getComputedStyle(el).animationName` —— 只有这个才能反映「规则到底有没有活下来」。
|
||||
`src/scripts/back-to-top.ts` 就是这么做的,CSS 那边一旦失效它会自动接管。
|
||||
|
||||
`pnpm build` 和 `deploy.sh` 都会跑 `scripts/verify-build-css.mjs` 拦截这类回退。
|
||||
|
||||
## 写作
|
||||
|
||||
内容集合定义在 `src/content/config.ts`,共三个:
|
||||
|
||||
@@ -84,6 +84,15 @@ if [ "$PAGES" -lt 5 ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# CSS 自检:拦「滚动驱动动画被压缩器并进 animation 简写」这类只在构建产物里出现、
|
||||
# 本地 dev 完全看不出来的回归(2026-09-21 的回到顶部按钮事故)。
|
||||
# pnpm build 里已经跑过一次,这里再跑是为了覆盖 --skip-build 直接用旧 dist/ 的情况。
|
||||
if ! node scripts/verify-build-css.mjs; then
|
||||
echo ""
|
||||
echo "❌ CSS 自检未通过,已中止部署(没有推送任何内容)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 3. 生产环境确认 ----------
|
||||
if [ "$DO_CLOUD" -eq 1 ] && [ "$DRY_RUN" -eq 0 ] && [ "$ASSUME_YES" -eq 0 ]; then
|
||||
echo ""
|
||||
|
||||
@@ -214,6 +214,11 @@ pnpm reading
|
||||
|
||||
读完的书会自动出现在「书籍」区底部的「微信读书 · 已读完」分组里,不用手工搬。
|
||||
|
||||
**有声书(专辑)**:同一次请求里还会抓书架上的专辑,写进 `reading.json` 的 `albums` 字段。
|
||||
「在读」页会自动多出一个「有声书」区块 —— 但没有专辑时整块不显示,所以现在看不到是正常的。
|
||||
专辑没有阅读进度也没有跳转链接(微信读书那边就没给),卡片只显示集数、完结状态和最近收听。
|
||||
`--top` 只作用于书,专辑一次全取;`secret: 1` 的私密收听同样会被过滤掉。
|
||||
|
||||
**平时不用管**:`pnpm build` 和 `./deploy.sh` 都会自动先抓一次。抓不到就沿用上一次的数据,**不会让部署失败**。
|
||||
|
||||
**首次配置**(只做一次):
|
||||
|
||||
+2
-1
@@ -6,9 +6,10 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "node scripts/fetch-reading.mjs --soft && astro build && pagefind --site dist",
|
||||
"build": "node scripts/fetch-reading.mjs --soft && astro build && node scripts/verify-build-css.mjs && pagefind --site dist",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"verify:css": "node scripts/verify-build-css.mjs",
|
||||
"new-post": "node scripts/new-post.mjs",
|
||||
"new-thought": "node scripts/new-thought.mjs",
|
||||
"reading": "node scripts/fetch-reading.mjs"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 抓取微信读书「最近在读」,写出 src/data/reading.json
|
||||
/* 抓取微信读书「最近在读」与「有声书专辑」,写出 src/data/reading.json
|
||||
*
|
||||
* 用法:
|
||||
* pnpm reading 手动刷新(失败会 exit 1,明确报错)
|
||||
@@ -17,14 +17,24 @@
|
||||
* 只有「阅读进度」要额外调 /book/getprogress,所以请求数 = 1 + N
|
||||
* 2. book.recordReadingTime 恒为 0,但 book.readingTime 是真数据,累计时长取后者
|
||||
* 3. 官方文档没列 book.startReadingTime,但回包里有,是「开始阅读时间」
|
||||
* 4. albums[](专辑/有声书)和 books[] 是平行数组,数据结构也不一样:
|
||||
* 内容包在 albumInfo / albumInfoExtra 两层里,且**没有进度、没有 deepLink**
|
||||
* (至少官方文档没写)。--top 只作用于 books,专辑一次全取。
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const ENDPOINT = "https://i.weread.qq.com/api/agent/gateway"
|
||||
/** 与官方 SKILL.md 顶部的 version 保持一致;服务端会用它对版本 */
|
||||
const SKILL_VERSION = "1.0.4"
|
||||
/**
|
||||
* 与官方 SKILL.md 顶部的 version 保持一致;服务端会用它对版本。
|
||||
*
|
||||
* 升级就是改这个数字,**不需要**下载官方的 weread-skills.zip:那个包是给 agent 读的
|
||||
* 提示词文档(SKILL.md + shelf.md / book.md 等),本项目一个文件都不消费它;
|
||||
* 网关只拿这里的 skill_version 做比对。2026-10-10 起服务端要求 1.0.5。
|
||||
* 参考:https://cdn.weread.qq.com/skills/weread-skills.zip
|
||||
*/
|
||||
const SKILL_VERSION = "1.0.5"
|
||||
const OUT_FILE = "src/data/reading.json"
|
||||
/** 0 = 不限制,整架都抓。书架一般也就几十本,1+N 个请求完全够用 */
|
||||
const DEFAULT_TOP = 0
|
||||
@@ -47,6 +57,54 @@ const apiKey = process.env.WEREAD_API_KEY
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
/**
|
||||
* 时间戳转 ISO。
|
||||
*
|
||||
* 微信读书的书架时间戳实测是**秒**;但专辑那边的 lectureReadUpdateTime 官方文档
|
||||
* 没写单位,万一是毫秒(13 位)就会得到公元 5 万年。用位数兜一下,两边都安全。
|
||||
*/
|
||||
function toIso(value) {
|
||||
const n = Number(value)
|
||||
if (!Number.isFinite(n) || n <= 0) return null
|
||||
return new Date(n > 1e12 ? n : n * 1000).toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 专辑/有声书。和 books[] 平行,但结构完全不同:内容在 albumInfo / albumInfoExtra 两层。
|
||||
*
|
||||
* 官方文档给的字段里没有 progress、也没有 deepLink,所以:
|
||||
* - 不伪造进度,前端只展示「集数 / 完结状态 / 最近收听」;
|
||||
* - deepLink 按几个可能的位置兜一下,取不到就是空串(卡片退化成不可点击)。
|
||||
* 私密收听(secret === 1)和书一样必须过滤掉 —— 这是要发到公开网站上的。
|
||||
*/
|
||||
function normalizeAlbums(shelf) {
|
||||
return (shelf.albums ?? [])
|
||||
.filter((album) => album?.albumInfoExtra?.secret !== 1)
|
||||
.map((album) => {
|
||||
const info = album.albumInfo ?? {}
|
||||
const extra = album.albumInfoExtra ?? {}
|
||||
return {
|
||||
albumId: info.albumId ?? "",
|
||||
name: info.name ?? "",
|
||||
/** 演播 / 作者 */
|
||||
author: info.authorName ?? "",
|
||||
cover: info.cover ?? "",
|
||||
trackCount: Number(info.trackCount) || 0,
|
||||
finished: Number(info.finish) === 1,
|
||||
/** 完结状态原文,如「已完结」 */
|
||||
finishStatus: info.finishStatus ?? "",
|
||||
intro: info.intro ?? "",
|
||||
lastListenedAt: toIso(extra.lectureReadUpdateTime),
|
||||
deepLink: info.deepLink ?? album.deepLink ?? "",
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const at = a.lastListenedAt ? Date.parse(a.lastListenedAt) : 0
|
||||
const bt = b.lastListenedAt ? Date.parse(b.lastListenedAt) : 0
|
||||
return bt - at
|
||||
})
|
||||
}
|
||||
|
||||
async function call(apiName, params = {}) {
|
||||
const res = await fetch(ENDPOINT, {
|
||||
method: "POST",
|
||||
@@ -61,7 +119,22 @@ async function call(apiName, params = {}) {
|
||||
const json = await res.json().catch(() => null)
|
||||
|
||||
if (json?.upgrade_info) {
|
||||
console.warn(`⚠️ 服务端要求升级 Skill:${json.upgrade_info.message ?? "(无说明)"}`)
|
||||
/*
|
||||
* 服务端只拿 skill_version 做版本比对。原来的写法只打了 message,把
|
||||
* current_version / latest_version / upgrade_url 一起丢掉了,于是提示退化成
|
||||
* 一句无解的「请立即升级」—— 而 message 本身是写给 agent 的通用话术
|
||||
* (「必须立即暂停当前操作…不得忽略该字段」),照做就等于让远端文案驱动本地文件。
|
||||
* 这里改成把可执行的信息全打出来:差了多少版、去哪儿看、改哪一行。
|
||||
*/
|
||||
const { current_version, latest_version, upgrade_url, message } = json.upgrade_info
|
||||
console.warn(
|
||||
`⚠️ 微信读书 Skill 版本过期:本地 ${current_version ?? SKILL_VERSION} → 服务端要求 ${latest_version ?? "?"}(接口仍可用,只是提醒)`,
|
||||
)
|
||||
if (message) console.warn(` ${message}`)
|
||||
if (upgrade_url) console.warn(` 升级包(仅供查阅,本项目不消费):${upgrade_url}`)
|
||||
console.warn(
|
||||
` 处理方式:把本文件顶部的 SKILL_VERSION 改成 "${latest_version ?? "最新版本"}",无需下载安装任何文件。`,
|
||||
)
|
||||
}
|
||||
if (res.status === 401 || json?.errcode === -2010) {
|
||||
throw new Error(
|
||||
@@ -80,6 +153,9 @@ async function call(apiName, params = {}) {
|
||||
async function fetchReading() {
|
||||
const shelf = await call("/shelf/sync")
|
||||
|
||||
// 专辑一次全取(--top 只管书),所以先算出来,避免下面提前 return 时把它丢了
|
||||
const albums = normalizeAlbums(shelf)
|
||||
|
||||
const sorted = (shelf.books ?? [])
|
||||
// 私密阅读(secret=1)不要发到公开网站上
|
||||
.filter((book) => book.secret !== 1)
|
||||
@@ -91,7 +167,7 @@ async function fetchReading() {
|
||||
const candidates = TOP_N > 0 ? sorted.slice(0, TOP_N) : sorted
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return { books: [], note: "书架里没有可展示的阅读记录" }
|
||||
return { books: [], albums, note: "书架里没有可展示的阅读记录" }
|
||||
}
|
||||
|
||||
const books = []
|
||||
@@ -140,7 +216,7 @@ async function fetchReading() {
|
||||
await sleep(DELAY_BETWEEN_CALLS_MS)
|
||||
}
|
||||
|
||||
return { books }
|
||||
return { books, albums }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -151,23 +227,27 @@ async function main() {
|
||||
)
|
||||
}
|
||||
|
||||
const { books, note } = await fetchReading()
|
||||
const { books, albums, note } = await fetchReading()
|
||||
if (note) console.warn(`⚠️ ${note}`)
|
||||
|
||||
const payload = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: "weread",
|
||||
books,
|
||||
albums,
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true })
|
||||
fs.writeFileSync(OUT_FILE, `${JSON.stringify(payload, null, 2)}\n`)
|
||||
|
||||
console.log(`✅ 写入 ${books.length} 本到 ${OUT_FILE}`)
|
||||
console.log(`✅ 写入 ${books.length} 本 + ${albums.length} 张有声书专辑到 ${OUT_FILE}`)
|
||||
for (const book of books) {
|
||||
const progress = book.progress === null ? " ?" : `${String(book.progress).padStart(3)}%`
|
||||
console.log(` ${progress} ${book.title}${book.finished ? "(已读完)" : ""}`)
|
||||
}
|
||||
for (const album of albums) {
|
||||
console.log(` 🎧 ${album.name}${album.trackCount ? `(${album.trackCount} 集)` : ""}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* 构建产物 CSS 自检:拦住「滚动驱动动画被压缩器并进 animation 简写」这一类静默回归。
|
||||
*
|
||||
* 背景(2026-09-21 的实际事故):
|
||||
* 线上「回到顶部」按钮不显示,本地 dev 却正常。
|
||||
* 原因是 Lightning CSS(Tailwind v4 的 CSS 压缩器)把
|
||||
* animation: back-to-top-reveal linear both;
|
||||
* animation-timeline: scroll(root block);
|
||||
* 合并成了
|
||||
* animation: linear both back-to-top-reveal scroll(root);
|
||||
* 而 WebKit 不接受 animation 简写里的 <single-animation-timeline> 分量
|
||||
* (webkit#293276),于是**整条 animation 声明被判无效丢弃**,元素永远停在
|
||||
* 初始的 opacity:0 / visibility:hidden。
|
||||
*
|
||||
* 最要命的是它只在构建产物里出现:astro dev 不做这个压缩,所以本地怎么试都正常;
|
||||
* 而 CSS.supports("animation-timeline: scroll()") 又照样返回 true,
|
||||
* 运行时特性检测也发现不了。见 lightningcss#815。
|
||||
*
|
||||
* 这个脚本在 build 之后跑,扫 dist 下所有 CSS:只要 animation 简写的值里出现
|
||||
* scroll() / view() 时间线函数,就让构建失败。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/verify-build-css.mjs # 失败时退出码 1
|
||||
* node scripts/verify-build-css.mjs --warn # 只警告,不失败
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const DIST = "./dist"
|
||||
const WARN_ONLY = process.argv.includes("--warn")
|
||||
|
||||
/** animation 简写里不该出现的时间线函数 */
|
||||
const TIMELINE_FN = /(?:^|[^-\w])(?:scroll|view)\(/
|
||||
|
||||
/** 收集 dist 下所有 css 文件 */
|
||||
function collectCssFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return []
|
||||
const found = []
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) found.push(...collectCssFiles(full))
|
||||
else if (entry.name.endsWith(".css")) found.push(full)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* 只检查 `animation:` 简写(以及 -webkit-animation:),不碰
|
||||
* `animation-timeline:` / `animation-range:` 这两个长写属性 —— 后者是正确写法。
|
||||
*/
|
||||
function findShorthandViolations(css) {
|
||||
const hits = []
|
||||
// 负向回顾避免匹配到 animation-timeline / animation-name 这类长写属性
|
||||
const re = /(?:^|[;{}(])\s*(?:-webkit-)?animation\s*:\s*([^;}]*)/g
|
||||
let match
|
||||
while ((match = re.exec(css)) !== null) {
|
||||
const value = match[1]
|
||||
if (TIMELINE_FN.test(value)) {
|
||||
hits.push(value.trim().slice(0, 120))
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
const files = collectCssFiles(DIST)
|
||||
if (files.length === 0) {
|
||||
console.error(`❌ ${DIST} 下没有找到 CSS,构建产物异常`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const problems = []
|
||||
for (const file of files) {
|
||||
const css = fs.readFileSync(file, "utf-8")
|
||||
for (const value of findShorthandViolations(css)) {
|
||||
problems.push({ file, value })
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length === 0) {
|
||||
console.log(`✅ 构建产物 CSS 自检通过(${files.length} 个文件,没有时间线函数混进 animation 简写)`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.error("❌ 构建产物 CSS 自检失败:animation 简写里混进了时间线函数。")
|
||||
console.error("")
|
||||
for (const { file, value } of problems) {
|
||||
console.error(` ${file}`)
|
||||
console.error(` animation: ${value}`)
|
||||
}
|
||||
console.error("")
|
||||
console.error(" 这类声明会被 WebKit 整条丢弃(webkit#293276),元素会停在初始样式上。")
|
||||
console.error(" 修法:把 animation-timeline / animation-range 挪到**独立的规则块**里,")
|
||||
console.error(" 不要和 animation 简写放在同一组声明中(lightningcss#815)。")
|
||||
console.error("")
|
||||
|
||||
process.exit(WARN_ONLY ? 0 : 1)
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
import { formatReadingDate, type ReadingAlbum } from "@/lib/reading"
|
||||
|
||||
/**
|
||||
* 微信读书「专辑 / 有声书」卡片。
|
||||
*
|
||||
* 为什么不复用 ReadingCard:两边能拿到的字段不一样。专辑没有 progress(官方结构里
|
||||
* 就没有),文档也没给 deepLink,还少了分类、开始/读完时间。硬塞进书卡会出现
|
||||
* 「进度条永远是 0%」这种假信息,所以单独一张,底部换成「集数 / 完结状态 / 最近收听」。
|
||||
*
|
||||
* deepLink 拿不到时不渲染成 <a>,只做展示 —— 一个点不动的链接比没有链接更糟。
|
||||
*/
|
||||
interface Props {
|
||||
album: ReadingAlbum
|
||||
}
|
||||
|
||||
const { album } = Astro.props
|
||||
|
||||
const Tag = album.deepLink ? "a" : "div"
|
||||
const linkable = Boolean(album.deepLink)
|
||||
const lastListened = formatReadingDate(album.lastListenedAt)
|
||||
const meta = [album.trackCount > 0 ? `${album.trackCount} 集` : "", album.finishStatus]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
---
|
||||
|
||||
<Tag
|
||||
href={linkable ? album.deepLink : undefined}
|
||||
target={linkable ? "_blank" : undefined}
|
||||
rel={linkable ? "noopener noreferrer" : undefined}
|
||||
class:list={[
|
||||
"group flex gap-4 rounded-xl border border-border/50 bg-card p-4",
|
||||
linkable && "transition-shadow duration-300 hover:shadow-lg",
|
||||
]}
|
||||
>
|
||||
<div class="shrink-0 w-20 aspect-square rounded-lg overflow-hidden bg-surface-subtle">
|
||||
{album.cover ? (
|
||||
<img
|
||||
src={album.cover}
|
||||
alt={album.name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerpolicy="no-referrer"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div class="flex h-full w-full items-center justify-center p-2 bg-gradient-to-br from-primary/30 via-primary/20 to-primary/5">
|
||||
<p class="font-semibold text-[10px] text-foreground text-center line-clamp-4">{album.name}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<h3
|
||||
class:list={[
|
||||
"font-medium text-sm text-foreground line-clamp-2 leading-snug mb-1",
|
||||
linkable && "group-hover:text-primary transition-colors",
|
||||
]}
|
||||
>
|
||||
{album.name}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground line-clamp-1 mb-3">{album.author}</p>
|
||||
|
||||
<div class="mt-auto space-y-1.5">
|
||||
{meta && <p class="text-[11px] text-muted-foreground/80 tabular-nums">{meta}</p>}
|
||||
<p class="text-[11px] text-muted-foreground/80 tabular-nums">
|
||||
{lastListened ? `最近收听 ${lastListened}` : "还没有收听记录"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tag>
|
||||
@@ -4,6 +4,7 @@ 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 AlbumCard from "@/components/album-card.astro"
|
||||
import { formatReadingDate, getReadingData } from "@/lib/reading"
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,11 @@ const inProgressReading = readingBooks.filter((book) => !book.finished)
|
||||
const finishedReading = readingBooks.filter((book) => book.finished)
|
||||
const readingCount = inProgressReading.length
|
||||
|
||||
// 有声书专辑:与 books[] 平行的另一组数据。
|
||||
// 用 ?? [] 兜住两种情况:老版 reading.json 没有 albums 字段(--soft 回退时),
|
||||
// 以及你根本没听过有声书(服务端返回空数组)。
|
||||
const readingAlbums = reading?.albums ?? []
|
||||
|
||||
/** 「书籍」区里给微信读书已读书记一个分类名,同时作为可筛选的领域 */
|
||||
const WEREAD_CATEGORY = "微信读书"
|
||||
|
||||
@@ -194,6 +200,27 @@ const recommendBadge = (status?: string) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ========================= 有声书(微信读书专辑) =========================
|
||||
与 books[] 平行的一组数据,只有真的听过有声书时服务端才会返回内容。
|
||||
readingAlbums 为空就整块不渲染 —— 所以在你有第一张专辑之前,这个区块
|
||||
既不存在也不占位。数据由 scripts/fetch-reading.mjs 一并抓取。 */}
|
||||
{isReadingView && readingAlbums.length > 0 && (
|
||||
<div id="albums" class="mb-16 scroll-mt-24">
|
||||
<div class="flex flex-wrap items-end justify-between gap-x-6 gap-y-2 mb-6">
|
||||
<div>
|
||||
<div class="text-[11px] text-primary tracking-[0.22em] font-semibold uppercase mb-1.5">Collection · 02</div>
|
||||
<h2 class="text-3xl md:text-4xl font-bold font-serif tracking-tight text-foreground mb-1.5">有声书</h2>
|
||||
<div class="text-sm italic font-serif text-muted-foreground">耳朵也在读书</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground tabular-nums">{readingAlbums.length} 张专辑</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{readingAlbums.map((album) => <AlbumCard album={album} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isReadingView && (
|
||||
<>
|
||||
{/* ---------- Header ---------- */}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: 关于 AI 的思考
|
||||
published: 2026-06-05
|
||||
image: ""
|
||||
tags: [AI, 思考]
|
||||
category: 思考
|
||||
draft: false
|
||||
---
|
||||
AI 发展越来越快,各式各样的 claw 龙虾🦞层出不穷、日新月异,真的是令人眼花缭乱,AI 工具很多,我们该怎么使用,这是一个新的问题。
|
||||
|
||||
<!-- more -->
|
||||
|
||||

|
||||
|
||||
像我现在使用的 picoclaw,仅仅是部署在我的树莓派上,似乎能完成的工作并不是很多,例如查询树莓派的运行情况,执行一些命令行,还有各种 Skills 没有使用,其应用潜力我只是开发了不到 10%,这是如此的神奇,Impssible😝。
|
||||
|
||||
## 一、智力不再是稀有属性
|
||||
|
||||
AI 现在对于我来说,最大的帮助就是能够在陌生领域下,靠提问来参考,完成之前认为比较复杂的问题,极大的降低了学习成本,或者说是入门的门槛。对人的智力解放是极大的,在 openclaw、hermes_Agent 这些工具我还不会使用,当然对词元的消耗也是巨大的。好像又回到了网络初始的年代,高昂的入网费用,多数人将流量用来娱乐而非创造价值。
|
||||
|
||||

|
||||
|
||||
现在似乎对程序员来说影响还是比较大的,AI 能够完成很多工作,似乎已经把人从简单重复的脑力劳动中解放出来。只有最优秀的、最会使用词元 Token 的人,能够将创意灵感迅速变现的人,才能适应这个时代。人的唯一性,灵感、创意、审美这些更重要的品质在智力成本降低的时候更加凸显。
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
title: 关于写作
|
||||
published: 2023-04-01
|
||||
image: ""
|
||||
tags: [思考, 写作]
|
||||
category: 思考
|
||||
draft: false
|
||||
---
|
||||
现阶段的计划就是先把自己的文章写好一些,怎么写材料,怎么写好材料可以说是我的努力方向之一了,当前这种情况下如果我还是放任自己浪费时间,到时候肯定会后悔的。学习专业知识,提升管理水平,锻炼写作能力,可以很好的解决目前所产生的焦虑感。
|
||||
|
||||
如何开始,我觉得必须现有一个计划,写作首先要有一个动力来源。我要养成一个记录的习惯,可以记录一些自己的想法和理解,每天的工作总结之类的东西。积少成多,自然而然的就能多写一些东西了。先在脑子里形成一个想法,然后就要列出一个框架、层次。干工作好多时候必须要讲思路,要有条理。我觉的通过写写东西可以锻炼这种能力。此外,单位的一些政治教育需要我自己主动去做教案,这也是可以通过去政工网上借鉴一些优秀的文章,从中学习行文结构和思路。文章写作可以先从模仿开始,逐渐掌握其中的门道,最后能够自己独立完成一篇原创文章。
|
||||
|
||||
现在我有了自己的这个小站,也可以写自己的一些东西,无论是生活感悟还是技术文章,或者是一些图片之类的,都可以在这个平台上记录,还是那句话,"**现在不缺记录的平台,缺乏记录的热情**。"
|
||||
@@ -2,10 +2,12 @@
|
||||
title: 博客到底可以用来做什么
|
||||
published: 2026-05-20
|
||||
image: ""
|
||||
description: "我现在在想,我折腾了好长时间,现在弄的这个博客都能做些什么呢?"
|
||||
tags: [思考, 博客]
|
||||
category: 建站
|
||||
draft: false
|
||||
---
|
||||
|
||||
我现在在想,我折腾了好长时间,现在弄的这个博客都能做些什么呢?我能够分享自己的什么呢?
|
||||
|
||||
这个是我需要思考的一些问题,人的精力是有限的,要平衡好工作、生活、爱好之前的关系,处理不好就容易陷入内耗空转的情况。
|
||||
@@ -19,3 +21,13 @@ Halo 用起来和 Vanblog 相比,功能确实很多,但运行起来也相对
|
||||
之后休假有时间,一定要好好研究一下博客主题是怎么设置的,这个 Halo 自带的主题缺点确实比较明显,walker 主题很不错,但只能专业版使用,凌霞公司真的🐶,靠这些插件的差异化功能赚钱,还必须绑定起来卖。价格也有点不合理,没事还是要备份一下数据,防止哪天痛失我的这个小站,当然也要对云服务器来个快照。
|
||||
|
||||
📝自己好像陷入了一个怪圈,老是在折腾来折腾去,浪费的是自己的精力,却并没有带来什么有价值的成果。安装这个应用,那个数据库,折腾博客 typecho、mcdocs,只是空耗,只是在浪费自己的时间。真正应该做的是多思考,多总结,干一些有意义的事情。重复造轮子的事情还是不要再干了。
|
||||
|
||||
现阶段的计划就是先把自己的文章写好一些,怎么写材料,怎么写好材料可以说是我的努力方向之一了,当前这种情况下如果我还是放任自己浪费时间,到时候肯定会后悔的。学习专业知识,提升管理水平,锻炼写作能力,可以很好的解决目前所产生的焦虑感。
|
||||
|
||||
如何开始,我觉得必须现有一个计划,写作首先要有一个动力来源。我要养成一个记录的习惯,可以记录一些自己的想法和理解,每天的工作总结之类的东西。积少成多,自然而然的就能多写一些东西了。先在脑子里形成一个想法,然后就要列出一个框架、层次。干工作好多时候必须要讲思路,要有条理。我觉的通过写写东西可以锻炼这种能力。此外,单位的一些政治教育需要我自己主动去做教案,这也是可以通过去政工网上借鉴一些优秀的文章,从中学习行文结构和思路。文章写作可以先从模仿开始,逐渐掌握其中的门道,最后能够自己独立完成一篇原创文章。
|
||||
|
||||
现在我有了自己的这个小站,也可以写自己的一些东西,无论是生活感悟还是技术文章,或者是一些图片之类的,都可以在这个平台上记录,还是那句话,"**现在不缺记录的平台,缺乏记录的热情**。"
|
||||
|
||||
>更新
|
||||
|
||||
今天是2026年10月6日,重新回头思考了一下,我觉得博客或者说自己的小站,更多的是个人的思考展示的一个具体平台。许多自己的想法和认识,用网页的形式公之于众,虽然不会有什么人去看,但是确实有一个独属于自己的赛博空间,用于自己耕耘。
|
||||
@@ -5,6 +5,20 @@ description: "我们这个时代为什么越来越繁忙,人工智能带来的
|
||||
slug: Improving-work-efficiency
|
||||
tags: [工作,人的异化]
|
||||
category: "思考"
|
||||
draft: true
|
||||
draft: false
|
||||
---
|
||||
我也在思考一个问题
|
||||
> 我很怀疑在AI时代写博客是否是一件有意义的事情。因为不论是谁,不论有什么问题,打开你身边的免费AI,问一下,一切结束 AI时代不需要任何非标准的答案,AI时代需要的是无穷的语料,官方文档,wiki,书,和代码 这事实上剥夺了普通人的创造力,只剩下大语言模型给我们带来的幻觉,我们会以为出现在Ai对话框里的任何回答都属于自己,知识不再需要系统的学习,碎片化的文字已经足够;非专家级别的成果/作品变得无用,既然AI能给标准答案,新手的尝试又有什么意义呢?为什么我不付钱给AI去完成这些工作呢?努力的意义正在被消解。 有人把ai定义为珍妮机一样的生产力工具,我觉得不然,生产力工具没法脱离生产者而生产,但是ai可以取代生产者
|
||||
|
||||
这是别人的博客中的一段评论,我觉得有一定的道理。确实AI时代,学习的过程被简化,导致学习能力的下滑是很严重的。例如,正常我要修改我的博客的一段代码,我得了解相关的编程知识,但是现在根本不需要去完整的掌握,自己似乎已经成为独立开发者,只需要会一些基础的知识即可。
|
||||
我也在思考一个问题,人工智能时代,个人的智力和创造力究竟要被置于何处。哪些是可以替代的,哪些又是无法使用AI来替代的呢?创造的过程在简化,结果更容易获得,与之相对应的则是学习的难度和惰性则会成倍增加,因为有着如此便捷的工具,这样的话实则是陷入了“知其然不知其所以然”的怪圈。就像小时候使用教辅资料直接抄答案一样,看似完成了作业,实则只是应付了事了。
|
||||
|
||||
AI 发展越来越快,各式各样的 claw 龙虾层出不穷、日新月异,真的是令人眼花缭乱,AI 工具很多,我们该怎么使用,这是一个新的问题。
|
||||
|
||||

|
||||
|
||||
像我现在使用的 picoclaw,仅仅是部署在我的树莓派上,似乎能完成的工作并不是很多,例如查询树莓派的运行情况,作为喂奶机器人的提醒等,可以实现很多自动化工作。
|
||||
AI 现在对于我来说,最大的帮助就是能够在陌生领域下,靠提问来参考,完成之前认为比较复杂的问题,极大的降低了学习成本,或者说是入门的门槛。对人的智力解放是极大的,在 openclaw、hermes_Agent 这些工具我还不会使用,当然对词元的消耗也是巨大的。好像又回到了网络初始的年代,高昂的入网费用,多数人将流量用来娱乐而非创造价值。
|
||||
|
||||
现在似乎对程序员来说影响还是比较大的,AI 能够完成很多工作,似乎已经把人从简单重复的脑力劳动中解放出来。只有最优秀的、最会使用词元 Token 的人,能够将创意灵感迅速变现的人,才能适应这个时代。人的唯一性,灵感、创意、审美这些更重要的品质在智力成本降低的时候更加凸显。
|
||||
|
||||
思考、学习,我们的一直以来成长的途径,在现在这个时代真的要发生巨变了吗?凡事都可以辨证分析,可以说事物的一体两面性的边界并不是很清晰。
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "LaTeX 数学公式使用教程"
|
||||
published: 2026-06-12
|
||||
description: "LaTeX 数学公式使用教程"
|
||||
image: ""
|
||||
tags: [教程, 工具]
|
||||
category: 教程
|
||||
draft: false
|
||||
---
|
||||
# LaTeX 数学公式使用教程
|
||||
|
||||
> 本站已启用 KaTeX 引擎,支持在文章中使用 LaTeX 语法书写数学公式。
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
title: Linux英语词汇速查手册
|
||||
published: 2026-06-11
|
||||
description: "Linux英语词汇速查手册,方便有的时候学习一下基础知识"
|
||||
image: ""
|
||||
tags: [Linux, 工具]
|
||||
category: ""
|
||||
category: "技术学习"
|
||||
draft: false
|
||||
---
|
||||
# 🐧 Linux 英语词汇速查手册
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 我的台式机到位了
|
||||
title: 我的台式机组装小记
|
||||
published: 2026-08-22
|
||||
description: 自己动手组装了一台计算机,也是赶在了硬件高价期,吃了一波AI税
|
||||
image: https://cos.tianxuetao.top/IMG_20260915_210045.avif
|
||||
@@ -35,8 +35,23 @@ slug: DIY-computer
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
机箱内部图片还是挺不错的,再加上RGB风扇后,现在一共是6组风扇,下进上出,侧进后出,整个风道还是挺流畅的,就是感觉CPU风冷是不是没有装好,打游戏的时候温度相对来说高一些(能达到70度)
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
现在又买了一块二手硬盘,希捷2TB的叠瓦盘,只花了378元,主要用于存储一些不重要的数》>据,可以多下载一些电影啥的,目前测试还不错没有坏道,可能是商家的擦除技术更加厉害了吧。就这样吧,现在这个情况下二手电子产品还是更具性价比。一块全新的硬盘价格是二手的3倍了(1200元上下),这个时间点真的是很奇怪,从内存条、到显卡、机械硬盘、固态硬盘的价格都是如此的夸张。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
>图一 · 健康度测试
|
||||
|
||||
|
||||

|
||||
>图二 · 硬盘坏块检测
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: 树莓派、云服务器的使用总结
|
||||
published: 2026-06-03
|
||||
published: 2026-05-03
|
||||
description: "利用大概一周多的时间,把 linux 系统、Docker 初步掌握了个七七八八,从之前买的树莓派 3B 作为硬件实体,逐步由简至深顺利搭建了属于自己的一个本地博客服务端。"
|
||||
image: ""
|
||||
tags: [树莓派, 云服务器, Linux]
|
||||
category: 技术
|
||||
@@ -36,7 +37,7 @@ Docker 容器化技术将各种依赖、运行环境打包成镜像,很多前
|
||||
|
||||
### 4. picoclaw 部署
|
||||
|
||||
**picoclaw** 也是一个伴随着"龙虾热🦞"诞生的一个开源项目,因为是 go 语言编译的,所以占用内存很低,轻量兼容性好,我也能够通过 `deb` 包的形式安装到我的树莓派上,现在我也是养了一只龙虾🦞。也同时学会了怎么配置大模型的 API,白嫖了智谱的 API 但是对请求量有限制,后面又转到爆火的 `Deepseek V4`,花了 10 元购买了 Token,接入后确实速度快了不少,现在就可以通过 `微信 calwbot` 来直接控制我的树莓派了。后面还会继续探讨智能体的影响。
|
||||
**picoclaw** 也是一个伴随着"龙虾热"诞生的一个开源项目,因为是 go 语言编译的,所以占用内存很低,轻量兼容性好,我也能够通过 `deb` 包的形式安装到我的树莓派上,现在我也是养了一只龙虾。也同时学会了怎么配置大模型的 API,白嫖了智谱的 API 但是对请求量有限制,后面又转到爆火的 `Deepseek V4`,花了 10 元购买了 Token,接入后确实速度快了不少,现在就可以通过 `微信 calwbot` 来直接控制我的树莓派了。后面还会继续探讨智能体的影响。
|
||||

|
||||
### 5. 云服务器和域名
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "执行力的影响"
|
||||
description: 坚定的去完成一件事,而不是消极应付。
|
||||
published: 2026-10-04
|
||||
tags:
|
||||
- "执行力,个人思考"
|
||||
category: "生活"
|
||||
slug: impact-of-execution
|
||||
|
||||
---
|
||||
坚定的执行
|
||||
|
||||
## 执行能力的重要性
|
||||
|
||||
我有时也会思考,人的一生靠什么来驱动。”人活一口气,树活一层皮“,说是人活着要争气,这气是什么,我认为是人的尊严,没有人希望自己如同草芥;其次是对命运的抗争,人生而不平等,出身寒微,但不能消极无为。要想改变这种情况,就需要执行力,坚持去做成一件事情。从学习到工作,都是需要憋着一口气去做。
|
||||
|
||||
|
||||
## 自己干成了的事情
|
||||
|
||||
执行力最终的体现在成事上,有一个结果出来。很多时候我所后悔的往往是因为中途放弃了,而最终没有实现的事情,这么久还是耿耿于怀。
|
||||
就拿五笔打字来说,自己现在还是在用拼音,只不过在AI加持下好像速度还行。但是办公来说还是太慢了,我还是没有练出来。
|
||||
|
||||
对自己的工作还是要上心,自己负责的事情,就要干出个结果来,如果以拖延来逃避,这就是执行力弱化的表现。工作本来就是一个克服阻力的过程,不会顺顺当当的就能完成,大多时候是需要加倍努力的💪,才能出色完成。
|
||||
|
||||
如果干工作一直拖拖拉拉,粗心大意只会让自己越来越差,生活工作一团糟。呈文办事,重在细心,标点符号、基本格式、文字用词如果都容易出现错误的话,确实不应该。有的时候这跟自己的工作态度有关,或者说缺少一种劲头,把工作干好的决心。自己反思了之后,仍然是觉得是主观原因占大多数,许多时候就是这样,个人的主观能动性还是很重要的一环。
|
||||
|
||||
## 知行合一的目标
|
||||
|
||||
**想到和得到之间还有一个做到**
|
||||
|
||||
有的时候我也在想这个问题,**做到**突出的是个人实现的过程,是改变这个物质世界的必然。很多时候,我会选择逃避,便会以拖延的形式出现。干工作没有动力,明明知道这件事情很重要了,但是却一拖再拖。
|
||||
|
||||
内心的驱动力就是这个中间体,映射到行动上就是执行力。自己需要不断提升自己的内驱动力,在自己的精神世界里里面不断建立一个又一个里程碑,完成一个又一个目标形成正反馈,让自己成为一个高执行力的人。
|
||||
很多时候,很多事情是很难看到回报的,或者说没有那么明显的正反馈,就容易懈怠,半途而废。
|
||||
然而每个人之间的差距就会体现在这里。引用一段话:每个人,每个年龄段,每个层次都有不熟悉的领域,不精通的专业。一部分人碍于面子不懂装懂,一部分人放低自己潜心学习。这与你的年龄段,与你的层次没有关系,只与你态度发生转变有关。
|
||||
|
||||
人非生而知之,
|
||||
|
||||
|
||||
现在就是感觉自己很迷茫,不想工作,但又不得不工作。
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
title: 记录自己的生活
|
||||
published: 2024-08-10
|
||||
image: ""
|
||||
tags: [生活]
|
||||
category: 生活
|
||||
draft: false
|
||||
---
|
||||

|
||||
|
||||
我现在拥有了最好的创作工具,**Obsidian** 和 **picoclaw**,现在这两个工具都能无缝对接到我的 Halo 小站了。通过个人令牌,能够实现如此强大的功能,确实令人惊叹。
|
||||
|
||||
Obsidian 现在作为一个创作平台,不仅可以将内容直接通过 Halo 插件发布到自己的小站,同时文章内容也是可以保存在本地,不会害怕数据丢失的风险。通过 PicoClaw 我这边可以直接通过微信发个瞬间之类的小短文,目前还没试过发文章,可以直接给它一个链接,让它总结文章内容转化我的小站文章。
|
||||
|
||||
兰空图床可以让文章插入更多的图片进行展示,让文章更加直观生动。
|
||||
|
||||
我现在拥有了一个相对稳定的记录小站,有了如此趁手的平台,更多的是要坚持写作,记录,无论是图片,还是经验总结,或者技术分享,都是很有价值的。可以多看看别人的博客文章,学习其优点。
|
||||
|
||||
多记录,多积累。
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
published: 2026-09-27T02:17:32.172Z
|
||||
location: "张掖"
|
||||
weather: "阴"
|
||||
tags:
|
||||
- "工作"
|
||||
---
|
||||
|
||||
时间过得很快,转眼就又要上班,似乎并没有完成一些有意义的事情。
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
published: 2026-10-04T12:09:10.254Z
|
||||
location: "张掖"
|
||||
tags:
|
||||
- "工作"
|
||||
---
|
||||
忙忙碌碌,却是原地踏步,要明白工作只认可结果,过程中的心酸苦辣无人在意。每个人都在向着更好的方向发展,自己也不能庸人自庸,自我麻痹。
|
||||
@@ -15,3 +15,5 @@ weather: 晴
|
||||
|
||||
## 我必须改变,这需要自己去行动起来
|
||||
|
||||
个人的工作态度直接决定着自己能不能通过工作取得进步,积极向上的态度对工作来说很重要,消极只会带来浑浑噩噩,时间非常宝贵。
|
||||
|
||||
|
||||
+134
-91
@@ -1,21 +1,147 @@
|
||||
{
|
||||
"updatedAt": "2026-09-21T01:32:16.851Z",
|
||||
"updatedAt": "2026-10-11T08:52:01.398Z",
|
||||
"source": "weread",
|
||||
"books": [
|
||||
{
|
||||
"bookId": "3300207468",
|
||||
"title": "快乐的死(果麦经典)",
|
||||
"author": "[法]加缪",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/12/cpplatform_tgx43jt54ezt7bamqcmjbk/t6_cpplatform_tgx43jt54ezt7bamqcmjbk1779878190.jpg",
|
||||
"progress": 5,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-10-08T15:27:08.000Z",
|
||||
"startedAt": null,
|
||||
"finishedAt": null,
|
||||
"readingSeconds": 262,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=66b32530813abb78ag018a22"
|
||||
},
|
||||
{
|
||||
"bookId": "3300086064",
|
||||
"title": "河西走廊",
|
||||
"author": "《河西走廊》摄制组编",
|
||||
"category": "历史-历史地理",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/89/cpplatform_htt13rz3ky797h8pvypqq1/t6_cpplatform_htt13rz3ky797h8pvypqq11707017069.jpg",
|
||||
"progress": 0,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-10-08T12:06:21.000Z",
|
||||
"startedAt": null,
|
||||
"finishedAt": null,
|
||||
"readingSeconds": 0,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=de932a80813ab881eg014870"
|
||||
},
|
||||
{
|
||||
"bookId": "3300175025",
|
||||
"title": "此刻是春天(微信读书特别版)",
|
||||
"author": "卢思浩",
|
||||
"category": "精品小说-治愈小说",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/4/cpplatform_3hsoybvrwapq1efqha1mvz/t6_cpplatform_3hsoybvrwapq1efqha1mvz1762325065.jpg",
|
||||
"progress": 4,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-10-08T12:05:30.000Z",
|
||||
"startedAt": "2026-01-11T10:32:09.000Z",
|
||||
"finishedAt": "2026-09-27T16:39:36.000Z",
|
||||
"readingSeconds": 295,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=b0b32630813abaadeg0157d1"
|
||||
},
|
||||
{
|
||||
"bookId": "3300095415",
|
||||
"title": "觉醒年代(全集|同名影视原著)",
|
||||
"author": "龙平平",
|
||||
"category": "历史-历史小说",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/66/cpplatform_4opftuyz4szxsagdfgsn1i/t6_cpplatform_4opftuyz4szxsagdfgsn1i1713258044.jpg",
|
||||
"progress": 0,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-10-08T11:44:34.000Z",
|
||||
"startedAt": null,
|
||||
"finishedAt": null,
|
||||
"readingSeconds": 0,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=e6032ec0813ab8bc5g015fd3"
|
||||
},
|
||||
{
|
||||
"bookId": "182788",
|
||||
"title": "北京法源寺(文学性与思想性相结合的经典作品)",
|
||||
"author": "李敖",
|
||||
"category": "历史-历史小说",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/16/yuewen_182788/t6_yuewen_1827881778320200.jpg",
|
||||
"progress": 100,
|
||||
"finished": true,
|
||||
"lastReadAt": "2026-10-08T11:42:40.000Z",
|
||||
"startedAt": "2026-09-21T06:26:14.000Z",
|
||||
"finishedAt": "2026-10-08T11:30:06.000Z",
|
||||
"readingSeconds": 11837,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=dcc3296052ca04dccdbef6c"
|
||||
},
|
||||
{
|
||||
"bookId": "26678299",
|
||||
"title": "大师和玛格丽特(果麦经典)",
|
||||
"author": "布尔加科夫",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://wfqqreader-1252317822.image.myqcloud.com/cover/299/26678299/t6_26678299.jpg",
|
||||
"progress": 62,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-09-20T16:46:27.000Z",
|
||||
"progress": 100,
|
||||
"finished": true,
|
||||
"lastReadAt": "2026-09-28T04:46:31.000Z",
|
||||
"startedAt": "2026-09-04T16:07:07.000Z",
|
||||
"finishedAt": null,
|
||||
"readingSeconds": 8729,
|
||||
"finishedAt": "2026-09-22T03:34:52.000Z",
|
||||
"readingSeconds": 14442,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=34c329807197141b34cf1db"
|
||||
},
|
||||
{
|
||||
"bookId": "23914527",
|
||||
"title": "树莓派开始,玩转Linux",
|
||||
"author": "Vamei 周昕梓",
|
||||
"category": "计算机-计算机综合",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/55/YueWen_23914527/t6_YueWen_23914527.jpg",
|
||||
"progress": 100,
|
||||
"finished": true,
|
||||
"lastReadAt": "2026-09-27T16:38:55.000Z",
|
||||
"startedAt": "2026-04-17T15:59:58.000Z",
|
||||
"finishedAt": "2026-09-27T16:37:54.000Z",
|
||||
"readingSeconds": 14629,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=140329d0716ce81f140468e"
|
||||
},
|
||||
{
|
||||
"bookId": "23796324",
|
||||
"title": "昨日的世界",
|
||||
"author": "茨威格",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/58/YueWen_23796324/t6_YueWen_23796324.jpg",
|
||||
"progress": 31,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-09-27T10:00:15.000Z",
|
||||
"startedAt": "2026-08-05T16:49:22.000Z",
|
||||
"finishedAt": null,
|
||||
"readingSeconds": 8117,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=80a324f0716b1a6480af682"
|
||||
},
|
||||
{
|
||||
"bookId": "3300176703",
|
||||
"title": "关于建国以来党的若干历史问题的决议",
|
||||
"author": "中国共产党第十一届中央委员会",
|
||||
"category": "政治军事-政治",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/6/cpplatform_bxvnmtl7wfftjdyxqgmpxt/t6_cpplatform_bxvnmtl7wfftjdyxqgmpxt1762827216.jpg",
|
||||
"progress": 77,
|
||||
"finished": false,
|
||||
"lastReadAt": "2026-09-22T03:36:37.000Z",
|
||||
"startedAt": "2026-06-03T15:44:54.000Z",
|
||||
"finishedAt": "2026-06-09T11:02:09.000Z",
|
||||
"readingSeconds": 2031,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=de6321e0813abab86g01378d"
|
||||
},
|
||||
{
|
||||
"bookId": "3300020417",
|
||||
"title": "西线无战事(译文经典)",
|
||||
"author": "雷马克",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/17/3300020417/t6_3300020417.jpg",
|
||||
"progress": 100,
|
||||
"finished": true,
|
||||
"lastReadAt": "2026-09-22T03:36:25.000Z",
|
||||
"startedAt": "2026-08-08T16:51:53.000Z",
|
||||
"finishedAt": "2026-09-06T05:26:12.000Z",
|
||||
"readingSeconds": 6214,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=a1932750813ab6e79g017e78"
|
||||
},
|
||||
{
|
||||
"bookId": "3300048195",
|
||||
"title": "正面管教(修订版)",
|
||||
@@ -30,48 +156,6 @@
|
||||
"readingSeconds": 20,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=bbc32a50813ab7953g01522a"
|
||||
},
|
||||
{
|
||||
"bookId": "3300176703",
|
||||
"title": "关于建国以来党的若干历史问题的决议",
|
||||
"author": "中国共产党第十一届中央委员会",
|
||||
"category": "政治军事-政治",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/6/cpplatform_bxvnmtl7wfftjdyxqgmpxt/t6_cpplatform_bxvnmtl7wfftjdyxqgmpxt1762827216.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "3300020417",
|
||||
"title": "西线无战事(译文经典)",
|
||||
"author": "雷马克",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/17/3300020417/t6_3300020417.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "23796324",
|
||||
"title": "昨日的世界",
|
||||
"author": "茨威格",
|
||||
"category": "文学-外国文学",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/58/YueWen_23796324/t6_YueWen_23796324.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "3300189057",
|
||||
"title": "我弥留之际(果麦经典)",
|
||||
@@ -86,20 +170,6 @@
|
||||
"readingSeconds": 589,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=b4432330813abb059g01728a"
|
||||
},
|
||||
{
|
||||
"bookId": "23914527",
|
||||
"title": "树莓派开始,玩转Linux",
|
||||
"author": "Vamei 周昕梓",
|
||||
"category": "计算机-计算机综合",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/55/YueWen_23914527/t6_YueWen_23914527.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "3300151878",
|
||||
"title": "从Python开始学编程",
|
||||
@@ -198,20 +268,6 @@
|
||||
"readingSeconds": 291,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=7c632ef05a49197c62b53f0"
|
||||
},
|
||||
{
|
||||
"bookId": "26943492",
|
||||
"title": "每天5分钟玩转Docker容器技术",
|
||||
"author": "CloudMan",
|
||||
"category": "计算机-计算机综合",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/41/YueWen_26943492/t6_YueWen_26943492.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "40055543",
|
||||
"title": "置身事内:中国政府与经济发展",
|
||||
@@ -254,20 +310,6 @@
|
||||
"readingSeconds": 4246,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=8033247071e0e1808035ab9"
|
||||
},
|
||||
{
|
||||
"bookId": "3300175025",
|
||||
"title": "此刻是春天(微信读书特别版)",
|
||||
"author": "卢思浩",
|
||||
"category": "精品小说-治愈小说",
|
||||
"cover": "https://cdn.weread.qq.com/weread/cover/4/cpplatform_3hsoybvrwapq1efqha1mvz/t6_cpplatform_3hsoybvrwapq1efqha1mvz1762325065.jpg",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"bookId": "39280063",
|
||||
"title": "博弈论:每个人都能成为决策高手(《人民日报》推荐)",
|
||||
@@ -366,5 +408,6 @@
|
||||
"readingSeconds": 137,
|
||||
"deepLink": "https://weread.qq.com/book-detail?type=1&v=47532ac05c659b47554a825"
|
||||
}
|
||||
]
|
||||
],
|
||||
"albums": []
|
||||
}
|
||||
|
||||
@@ -36,6 +36,39 @@ export type ReadingData = {
|
||||
updatedAt: string
|
||||
source: string
|
||||
books: ReadingBook[]
|
||||
/**
|
||||
* 专辑 / 有声书。
|
||||
*
|
||||
* 可选:`--soft` 构建路径下用的可能是更早版本的 reading.json,那时还没有这个字段。
|
||||
* 调用方一律写 `data.albums ?? []`。
|
||||
*/
|
||||
albums?: ReadingAlbum[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 一张有声书专辑。
|
||||
*
|
||||
* 字段刻意比 ReadingBook 少 —— 微信读书给专辑的原始结构里(albumInfo /
|
||||
* albumInfoExtra)**没有阅读进度,也没有 deepLink**,所以这里没有 progress,
|
||||
* 前端也不该伪造一条进度条。哪天官方补了字段,先改 scripts/fetch-reading.mjs。
|
||||
*/
|
||||
export type ReadingAlbum = {
|
||||
albumId: string
|
||||
name: string
|
||||
/** 演播 / 作者 */
|
||||
author: string
|
||||
cover: string
|
||||
/** 音频集数;取不到时为 0 */
|
||||
trackCount: number
|
||||
/** 是否已完结(原始字段 finish === 1) */
|
||||
finished: boolean
|
||||
/** 完结状态原文,如「已完结」 */
|
||||
finishStatus: string
|
||||
intro: string
|
||||
/** 最近收听时间,ISO;从没听过时为 null */
|
||||
lastListenedAt: string | null
|
||||
/** 跳转链接。官方文档未列此字段,实测可能为空串 */
|
||||
deepLink: string
|
||||
}
|
||||
|
||||
const DATA_FILE = path.join(process.cwd(), "src/data/reading.json")
|
||||
|
||||
@@ -7,16 +7,31 @@ import { onPage } from "./lifecycle"
|
||||
* 淡入,不需要任何 scroll 监听(见 global.css 的 .back-to-top)。
|
||||
* 这里只负责两件事:
|
||||
* 1. 点击后平滑回到顶部;
|
||||
* 2. 给不支持滚动驱动动画的浏览器(Firefox < 144 / Safari < 26)做降级。
|
||||
* 2. 兜住「CSS 那条动画没生效」的所有情况 —— 既包括不支持滚动驱动动画的浏览器
|
||||
* (Firefox < 144 / Safari < 26),也包括规则被构建压缩弄坏的情况(见下)。
|
||||
*/
|
||||
|
||||
/** 与 global.css 里 animation-range 的起点保持一致 */
|
||||
/** 与 global.css 里 animation-range 的起点呼应 */
|
||||
const REVEAL_AFTER_PX = 480
|
||||
|
||||
const supportsScrollTimeline = () =>
|
||||
typeof CSS !== "undefined" &&
|
||||
typeof CSS.supports === "function" &&
|
||||
CSS.supports("animation-timeline: scroll()")
|
||||
/** global.css 中 @keyframes 的名字,用来实测动画有没有真的挂上 */
|
||||
const ANIMATION_NAME = "back-to-top-reveal"
|
||||
|
||||
/**
|
||||
* 「CSS 那条滚动驱动动画到底有没有生效」——直接读计算样式,而不是做特性检测。
|
||||
*
|
||||
* 为什么不用 CSS.supports("animation-timeline: scroll()"):它只回答「这个语法浏览器认不认」,
|
||||
* 回答不了「这条规则有没有在构建压缩中活下来」。实际的坑是:Lightning CSS 会把
|
||||
* animation-timeline 合并进 animation 简写,而 WebKit 不认简写里的 timeline 分量,
|
||||
* 整条声明被丢弃 —— 此时 CSS.supports 仍然返回 true,于是降级分支永远不执行,
|
||||
* 按钮就永远停在 opacity:0。改读 animationName 之后,CSS 那边无论因为什么原因没生效,
|
||||
* 这里都能立刻发现并接管(详见 global.css 里 .back-to-top 的注释)。
|
||||
*/
|
||||
const cssDrivesVisibility = (button: HTMLElement) =>
|
||||
window
|
||||
.getComputedStyle(button)
|
||||
.animationName.split(",")
|
||||
.some((part) => part.trim() === ANIMATION_NAME)
|
||||
|
||||
onPage("back-to-top", () => {
|
||||
const button = document.querySelector<HTMLElement>("[data-back-to-top]")
|
||||
@@ -28,7 +43,7 @@ onPage("back-to-top", () => {
|
||||
}
|
||||
button.addEventListener("click", onClick)
|
||||
|
||||
if (supportsScrollTimeline()) {
|
||||
if (cssDrivesVisibility(button)) {
|
||||
return () => button.removeEventListener("click", onClick)
|
||||
}
|
||||
|
||||
|
||||
+33
-1
@@ -1142,6 +1142,18 @@ a.card-github:hover .github-logo {
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.reading-progress-bar {
|
||||
animation: reading-progress-grow linear both;
|
||||
}
|
||||
/*
|
||||
* ⚠️ 这条规则的选择器故意比上面多一层 `.reading-progress >`,不是笔误。
|
||||
*
|
||||
* Lightning CSS 会合并「相邻且选择器完全相同」的规则:把 animation-timeline 并进
|
||||
* 上面那条的 animation 简写,产物变成 `animation: … reading-progress-grow scroll(root)`,
|
||||
* 而 WebKit 不认简写里的 timeline 分量,整条声明被丢弃 → 进度条永远 scaleX(0)。
|
||||
* 选择器不同就没法合并。这里用后代选择器而不是 `:where()`,是为了让特异性**不低于**
|
||||
* 上面那条 —— animation 简写会把 animation-timeline 重置回 auto,特异性更低会被重置压掉。
|
||||
* 完整背景见下面 .back-to-top 段的长注释,以及 scripts/verify-build-css.mjs。
|
||||
*/
|
||||
.reading-progress > .reading-progress-bar {
|
||||
animation-timeline: scroll(root block);
|
||||
}
|
||||
}
|
||||
@@ -1229,8 +1241,28 @@ a.card-github:hover .github-logo {
|
||||
/* 显隐交给滚动时间线,这里只留下颜色类过渡 */
|
||||
transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
|
||||
animation: back-to-top-reveal linear both;
|
||||
}
|
||||
/*
|
||||
* ⚠️⚠️ animation-timeline / animation-range 必须写在**独立的规则块**里,
|
||||
* 不能和上面的 animation 简写放在同一组声明中。
|
||||
*
|
||||
* Lightning CSS(Tailwind v4 用的 CSS 压缩器)会把同一组声明里的 animation-timeline
|
||||
* 合并进 animation 简写,构建产物变成:
|
||||
* animation: linear both back-to-top-reveal scroll(root)
|
||||
* 而 WebKit 目前不接受简写里的 <single-animation-timeline> 分量(webkit#293276),
|
||||
* 于是**整条 animation 声明被判无效并丢弃** —— 按钮永远停在 opacity:0 /
|
||||
* visibility:hidden。更坑的是 CSS.supports("animation-timeline: scroll()") 依然返回
|
||||
* true,纯特性检测完全看不出问题;dev 模式不做这个压缩,所以只有构建产物会踩到。
|
||||
* 参考 lightningcss#815。scripts/verify-build-css.mjs 会在构建后拦截这种合并回退。
|
||||
*
|
||||
* ⚠️ 所以这条规则的选择器故意写成 `.back-to-top[data-back-to-top]`(多一个属性选择器)
|
||||
* —— 与上面那条选择器不同,压缩器就无法把它们合并。刻意用属性选择器而不是 :where(),
|
||||
* 是为了让特异性**高于**上面那条:animation 简写会把 animation-timeline 重置回 auto,
|
||||
* 特异性持平或更低都会被那个重置压掉。
|
||||
*/
|
||||
.back-to-top[data-back-to-top] {
|
||||
animation-timeline: scroll(root block);
|
||||
/* 滚过 320px 开始淡入,620px 完全就位(与 back-to-top.ts 的 480px 阈值呼应) */
|
||||
/* 滚过 320px 开始淡入,620px 完全就位(与 back-to-top.ts 的降级阈值呼应) */
|
||||
animation-range: 320px 620px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user