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:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 读书模块的**写操作与子资源**(鸿蒙端)
|
||||
*
|
||||
* 对应服务端 `app/ApiHandler.php` 的 `books()`:
|
||||
* GET /api/v1/books 书架(支持 status / category / owner_id / keyword)
|
||||
* POST /api/v1/books 加一本(title / author / category / status)
|
||||
* GET /api/v1/books/<id> 一本
|
||||
* PATCH /api/v1/books/<id> 改(**只认 status / rating / current_page 三个键**)
|
||||
* DELETE /api/v1/books/<id> 删(连带封面与文件)
|
||||
* GET /api/v1/books/<id>/notes 这本书的读书笔记(划线 / 想法 / 书评)
|
||||
* POST /api/v1/books/<id>/notes 记一条(content / kind / chapter / loc)
|
||||
*
|
||||
* 🔴 用户 2026-10-20:「其他的功能也逐步完成……设置也没有任何的东西。」
|
||||
* 读书页那时是**纯书架**:书行点不动、没有"加一本"、设置三项全灰。
|
||||
*
|
||||
* ⚠️⚠️ 两处**服务端能力边界**(写在这里,免得以后又摆出假入口):
|
||||
* 1. `PATCH` **不能改书名/作者** —— `books()` 只从请求体取
|
||||
* `status` / `current_page` / `rating`,其它键根本不读。
|
||||
* (改书名要等接口扩,别在界面上放个"书名"输入框然后存不进去。)
|
||||
* 2. `PATCH` 里的 `current_page` **不对应任何列** —— `books` 表只有
|
||||
* `progress`(读到第几章),而 `Book::update()` 不读 `current_page`
|
||||
* ⇒ 端上**不要**回写章节进度,只**显示**。
|
||||
* 3. 上传电子书 / EPUB 解析 / 笔记导入 / 微信读书同步都在网页端,
|
||||
* 移动端没有文件选择与解析链路(`BookParser`、`BookNoteImport` 只有网页在调)。
|
||||
*/
|
||||
|
||||
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
||||
|
||||
/**
|
||||
* 加一本书。
|
||||
*
|
||||
* @param status 必须用**服务端枚举**:`wish` / `reading` / `done` / `paused`
|
||||
* (服务端 `Book::STATUS`;传别的会被 `Book::status()` **静默改成 `wish`**)
|
||||
*/
|
||||
export async function createBook(token: string, title: string, author: string,
|
||||
status: string): Promise<FbApiResult> {
|
||||
/* ⚠️ `arkts-no-untyped-obj-literals`:请求体必须 `Record<string, Object> = {}` 逐键赋值 */
|
||||
const body: Record<string, Object> = {};
|
||||
body['title'] = title;
|
||||
body['author'] = author;
|
||||
body['status'] = status;
|
||||
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||
return await fbApi('/api/v1/books', opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 改一本书的状态 / 评分。
|
||||
*
|
||||
* @param status 服务端枚举之一
|
||||
* @param rating 服务端是 **0–10**(端上 `FbBookItem.rate` 是 0–5 ⇒ 调用方乘 2);
|
||||
* 传 0 表示"取消评分"(`Book::rating()` 把 0 当合法值,落库为 0)
|
||||
*/
|
||||
export async function updateBook(token: string, id: number, status: string,
|
||||
rating: number): Promise<FbApiResult> {
|
||||
const body: Record<string, Object> = {};
|
||||
body['status'] = status;
|
||||
body['rating'] = rating;
|
||||
const opts: FbApiOptions = { method: 'PATCH', token: token, body: body };
|
||||
return await fbApi('/api/v1/books/' + id.toString(), opts);
|
||||
}
|
||||
|
||||
/** 删一本书(服务端会连带删封面与原书文件) */
|
||||
export async function deleteBook(token: string, id: number): Promise<FbApiResult> {
|
||||
const opts: FbApiOptions = { method: 'DELETE', token: token };
|
||||
return await fbApi('/api/v1/books/' + id.toString(), opts);
|
||||
}
|
||||
|
||||
/** 这本书的读书笔记(服务端直接返回数组,不是 `{items:…}` 信封) */
|
||||
export async function fetchBookNotes(token: string, id: number): Promise<FbApiResult> {
|
||||
const opts: FbApiOptions = { token: token };
|
||||
return await fbApi('/api/v1/books/' + id.toString() + '/notes', opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记一条读书笔记。
|
||||
*
|
||||
* @param kind `quote` 划线 / `note` 想法 / `review` 书评(服务端 `Book::NOTE_KINDS`,
|
||||
* 传别的会被改成 `note`)
|
||||
*/
|
||||
export async function addBookNote(token: string, id: number, content: string,
|
||||
kind: string): Promise<FbApiResult> {
|
||||
const body: Record<string, Object> = {};
|
||||
body['content'] = content;
|
||||
body['kind'] = kind;
|
||||
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||
return await fbApi('/api/v1/books/' + id.toString() + '/notes', opts);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user