v67:读书模块补齐(书能点进去 / 加一本 / 读书笔记 / 统计)

用户 2026-10-20:「其他的功能也逐步完成……读书点不进去,也没法加书,
设置也没有任何的东西。」

## 先修三处「对着服务端抄错的口径」(都不报错,只是显示是错的)
1. 状态值:服务端 `Book::STATUS` 只有 wish/reading/done/paused,**没有 want**;
   `Book::status()` 遇到不认识的值会静默改成 `wish`,而端上分组键写的 `want`
   ⇒ 书架里「想读」那一组**整组不显示**(页头还数着它,看着像书丢了)。
   `paused`(搁置)同样没列 ⇒ 搁置的书也人间蒸发。
2. 评分:服务端 `rating` 是 0–10,端上当 0–5 用 ⇒ 8 分显示成 `8.0/5`。
   现在解析时折半、写回时 ×2。
3. 进度:服务端 `progress` 是「读到第几章」,端上当百分比 ⇒ 读到第 30 章 = 30%。
   现在按 `chapter_count` 换算,没解析过章节的书不画假进度。

## 补齐三个死掉的入口
- 书行整行可点 → `FbBookSheet`(状态 / 评分 / 章节进度 / 读书笔记 / 删除二次确认)
- 页头「+ 加一本」→ 同一浮层的新建态(书名 / 作者 / 状态)
- 设置「读书统计」→ 端上现算(藏书/在读/读完/想读/搁置/笔记条数);
  另两条(导入笔记 / 微信读书)要文件选择与授权,仍标「网页端」,不摆假入口

## 服务端能力边界(照实做)
- `PATCH /books/<id>` 只认 status / rating ⇒ 详情里不给改书名/作者的输入框
- 章节进度只读(`progress` 由阅读器翻页写,接口没有回写入口)
- 读书笔记走真实接口 `GET/POST /books/<id>/notes`

## 验证
- `hvigorw assembleHap` → BUILD SUCCESSFUL
- `_fb_arkts_guard.js` → 797 / 0(新增 groupV67 28 条,含「端上分组键 ==
  服务端枚举」的跨语言对照)
- `_fb_v59d_falsify.py` → 130 / 0(v67 新增 13 条注入全部变红)
- ⚠️ 设备仍断连(`hdc list targets` → `[Empty]`),真机未验收
This commit is contained in:
2026-10-20 17:14:40 +08:00
parent 58f5509c1c
commit ec2cb14c4c
7 changed files with 1013 additions and 31 deletions
+46 -6
View File
@@ -15,7 +15,7 @@
*/
import {
FbAccount, FbAnniversaryItem, FbBookItem, FbCalEvent, FbHomeData, FbKinChip,
FbAccount, FbAnniversaryItem, FbBookItem, FbBookNoteItem, FbCalEvent, FbHomeData, FbKinChip,
FbKinGraph, FbKinPerson, FbMark,
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNotificationItem, FbResourceItem,
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
@@ -199,6 +199,15 @@ interface RawBook {
id?: number; title?: string; author?: string | null; status?: string | null;
rating?: number | null; progress?: number | null; note_count?: number | null;
cover_key?: string | null; category?: string | null; publisher?: string | null;
/** 共几章(0 = 没解析过章节)。`progress` 是"读到第几章",没有它就没法算百分比 */
chapter_count?: number | null;
}
interface RawBookNote {
id?: number; kind?: string | null; content?: string | null;
chapter?: string | null; loc?: string | null;
person_name?: string | null; my_nickname?: string | null;
created_at?: string | null;
}
interface RawNote {
@@ -507,23 +516,54 @@ export function parseBooks(json: string): FbBookItem[] {
const rows: RawBook[] = asArray<RawBook>(json);
const out: FbBookItem[] = [];
for (const r of rows) {
const st: string = s(r.status) !== '' ? s(r.status) : 'want';
/* 🔴🔴 v67:回落值必须是 **`wish`**(服务端枚举),原来写的 `want` 服务端不认
⇒ `Book::status()` 把不认识的静默改成 `wish`,而端上分组只认 `want`
⇒ "想读"的书在书架里一本都看不见。 */
const st: string = s(r.status) !== '' ? s(r.status) : 'wish';
const chapters: number = n(r.chapter_count);
const cur: number = n(r.progress);
const it: FbBookItem = {
id: n(r.id),
title: s(r.title),
author: s(r.author),
status: st,
rate: n(r.rating),
/* 🔴 服务端 `rating` 是 0–10(`Book::rating()` 校验 `0 <= n <= 10`),
端上 `FbRating` 的 max 是 5 ⇒ 必须折半,否则 8 分显示成 `8.0/5`。 */
rate: clamp(n(r.rating) / 2, 0, 5),
noteCount: n(r.note_count),
// 服务端 progress 记的是"读到第几页"还是"百分比"不确定
// ⇒ 只信 status:读完就是 100,否则按 progress 原样(0~100 之外夹紧)
progress: st === 'done' ? 100 : clamp(n(r.progress), 0, 100),
/* 🔴 `progress` 是**章序号**不是百分比(`Book::setProgress` 注释:读到第几章)
⇒ 有 `chapter_count` 才能换算;没解析过章节(=0)就只能是 0,
不要拿章序号冒充百分比(读到第 30 章 ≠ 30%)。 */
progress: st === 'done' ? 100
: (chapters > 0 ? clamp(cur * 100 / chapters, 0, 100) : 0),
curChapter: cur,
chapters: chapters,
};
out.push(it);
}
return out;
}
/** 读书笔记 → FbBookNoteItem[] */
export function parseBookNotes(json: string): FbBookNoteItem[] {
const rows: RawBookNote[] = asArray<RawBookNote>(json);
const out: FbBookNoteItem[] = [];
for (const r of rows) {
const nick: string = s(r.my_nickname);
out.push({
id: n(r.id),
kind: s(r.kind) !== '' ? s(r.kind) : 'note',
content: s(r.content),
chapter: s(r.chapter),
loc: s(r.loc),
/* 我称呼这个人的名字优先(`nicknames` 二元组),没有再退回他的本名 */
personName: nick !== '' ? nick : s(r.person_name),
createdAt: s(r.created_at).substring(0, 10),
});
}
return out;
}
/** 笔记 → FbNoteItem[] */
export function parseNotes(json: string): FbNoteItem[] {
const root: Record<string, Object> | null = asObject(json);