From ec2cb14c4cde205330c25c6e82ffd9502ad94900 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 20 Oct 2026 17:14:40 +0800 Subject: [PATCH] =?UTF-8?q?v67=EF=BC=9A=E8=AF=BB=E4=B9=A6=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E8=A1=A5=E9=BD=90=EF=BC=88=E4=B9=A6=E8=83=BD=E7=82=B9?= =?UTF-8?q?=E8=BF=9B=E5=8E=BB=20/=20=E5=8A=A0=E4=B8=80=E6=9C=AC=20/=20?= =?UTF-8?q?=E8=AF=BB=E4=B9=A6=E7=AC=94=E8=AE=B0=20/=20=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 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/` 只认 status / rating ⇒ 详情里不给改书名/作者的输入框 - 章节进度只读(`progress` 由阅读器翻页写,接口没有回写入口) - 读书笔记走真实接口 `GET/POST /books//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]`),真机未验收 --- entry/src/main/ets/components/FbBookSheet.ets | 680 ++++++++++++++++++ entry/src/main/ets/data/FbBookRepo.ets | 87 +++ entry/src/main/ets/data/FbParse.ets | 52 +- entry/src/main/ets/model/DemoData.ets | 30 +- entry/src/main/ets/model/FbSettingsMenus.ets | 17 +- entry/src/main/ets/model/Types.ets | 41 +- .../src/main/ets/pages/books/FbBooksPage.ets | 137 +++- 7 files changed, 1013 insertions(+), 31 deletions(-) create mode 100644 entry/src/main/ets/components/FbBookSheet.ets create mode 100644 entry/src/main/ets/data/FbBookRepo.ets diff --git a/entry/src/main/ets/components/FbBookSheet.ets b/entry/src/main/ets/components/FbBookSheet.ets new file mode 100644 index 0000000..f491ec1 --- /dev/null +++ b/entry/src/main/ets/components/FbBookSheet.ets @@ -0,0 +1,680 @@ +/** + * 家书 FamilyBook · 读书浮层:一本书(新建 / 详情 / 改 / 删 / 读书笔记)+ 读书统计 + * + * 对应网页版 `templates/books/index.php` 的 `#bookEdit` 弹框与 `books/notes.php`。 + * + * 🔴 用户 2026-10-20:「其他的功能也逐步完成……读书点不进去,也没法加书。」 + * 端上原来只有**书架**:书行没有 `onClick`、页头没有"加一本"、 + * 设置里三条 `action` 全是空串 ⇒ 三个入口全是死的。 + * + * ⚠️⚠️ **服务端能力边界**(照实做,不摆假入口): + * · `PATCH /books/` 只认 `status` / `rating` + * ⇒ 详情里**不提供改书名/作者**的输入框(改了也存不进去,比没做更糟), + * 只显示书名 + 一句"书名在网页端改"。 + * · 章节进度是**只读**的(`progress` 由阅读器翻页写,接口没有回写入口)。 + * · 上传 EPUB / 笔记导入 / 微信读书同步都在网页端(要文件选择与解析)。 + * + * ⚠️ 状态取值必须是服务端枚举 `wish / reading / done / paused`, + * **不能写 `want`**(服务端会静默改成 `wish`,端上分组又认不出 `wish`)。 + */ + +import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme'; +import { FbSheetClose } from './FbSheetClose'; +import { FbSheetRing } from './FbSheetRing'; +import { FB_TOKEN_STORE } from '../data/FbTokenStore'; +import { createBook, updateBook, deleteBook, fetchBookNotes, addBookNote } from '../data/FbBookRepo'; +import { parseBookNotes } from '../data/FbParse'; +import { FbApiResult } from '../data/FbHttp'; +import { FbBookItem, FbBookNoteItem } from '../model/Types'; + +/** 状态选项:`key` 必须与服务端 `Book::STATUS` 逐字一致 */ +const FB_BOOK_STATUS: string[] = ['wish', 'reading', 'done', 'paused']; +const FB_BOOK_STATUS_LABEL: string[] = ['想读', '在读', '读完', '搁置']; +/** 评分星:1..5(服务端 0–10,写回时 ×2) */ +const FB_BOOK_STARS: number[] = [1, 2, 3, 4, 5]; +/** 笔记种类(服务端 `Book::NOTE_KINDS`) */ +const FB_NOTE_KINDS: string[] = ['note', 'quote']; +const FB_NOTE_KIND_LABEL: string[] = ['想法', '划线']; + +@ComponentV2 +export struct FbBookSheet { + /** 模块主题色(读书 = 藕紫) */ + @Param accent: string = '#6B5B95'; + /** 书的 id(0 = 加一本) */ + @Param editingId: number = 0; + @Param initTitle: string = ''; + @Param initAuthor: string = ''; + /** 服务端枚举之一 */ + @Param initStatus: string = 'wish'; + /** 0–5 星(服务端 0–10 折半后) */ + @Param initRate: number = 0; + /** 读到第几章 / 共几章(只读展示) */ + @Param curChapter: number = 0; + @Param chapters: number = 0; + + @Event onClose: () => void = (): void => {}; + @Event onSaved: () => void = (): void => {}; + @Event onDeleted: () => void = (): void => {}; + + @Local fTitle: string = ''; + @Local fAuthor: string = ''; + /** 当前选中的状态(改动后点保存才提交) */ + @Local fStatus: string = 'wish'; + /** 当前星级 0–5(0 = 未评) */ + @Local fStars: number = 0; + @Local saving: boolean = false; + @Local confirmingDelete: boolean = false; + @Local note: string = ''; + + // ---- 读书笔记(只有详情态才有) ---- + @Local notes: FbBookNoteItem[] = []; + @Local notesLoading: boolean = false; + @Local notesNote: string = ''; + /** 新笔记的草稿与种类 */ + @Local draft: string = ''; + @Local draftKind: string = 'note'; + /** 正在提交笔记(与「保存这本书」分开:两个请求互不影响) */ + @Local storing: boolean = false; + + private isNew(): boolean { + return this.editingId <= 0; + } + + aboutToAppear(): void { + this.fTitle = this.initTitle; + this.fAuthor = this.initAuthor; + this.fStatus = this.initStatus !== '' ? this.initStatus : 'wish'; + this.fStars = this.initRate; + if (this.editingId > 0) { + this.loadNotes(); + } + } + + build() { + /* 🔴🔴 根必须 `Stack`(叠),不能 `Column`(排)—— + 全屏遮罩当 Column 子项会把卡片挤出屏幕,见 pitfalls.md */ + Stack({ alignContent: Alignment.Center }) { + Column() + .width('100%') + .height('100%') + .backgroundColor(FB_MASK) + .onClick((): void => { + this.onClose(); + }) + + Column() { + FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) { + Column({ space: 10 }) { + // ---- 标题栏 ---- + Row({ space: 8 }) { + Column({ space: 2 }) { + Text(this.isNew() ? '加一本书' : this.fTitle) + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text(this.subtitleOf()) + .fontSize(FbMetric.fontTiny) + .fontColor(this.accent) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + .flexShrink(1) + + FbSheetClose({ + onClose: (): void => { + this.onClose(); + }, + color: this.accent + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Scroll() { + Column({ space: 12 }) { + // ---- 加一本:书名 / 作者可填 ---- + if (this.isNew()) { + /* ⚠️ 不传"当前值"进来:`@Builder` 的按值参数**不参与状态观察**, + 输入框的 `text` 一律在 builder 内部直接读 `this.fTitle` / `this.fAuthor` + (否则打字时框里不刷新 —— v66 车辆表单踩过)。 */ + this.labeledInput('书名', '如:置身事内', true) + this.labeledInput('作者', '可不填', false) + } + + // ---- 状态 ---- + Row({ space: 8 }) { + Text('状态') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width(34) + Row({ space: 8 }) { + ForEach(FB_BOOK_STATUS, (k: string, idx: number) => { + this.statusChip(k, FB_BOOK_STATUS_LABEL[idx]) + }, (k: string): string => 'bs_' + k) + } + .layoutWeight(1) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + // ---- 评分(0–5 星,写回时 ×2 成服务端的 0–10)---- + Row({ space: 8 }) { + Text('评分') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width(34) + Row({ space: 4 }) { + ForEach(FB_BOOK_STARS, (i: number) => { + Text('★') + .fontSize(22) + .fontColor(i <= this.fStars ? '#C08A2E' : '#D9D2C2') + .onClick((): void => { + // 再点同一颗 = 取消这一档(退回上一档),点第 1 颗时归零 + this.fStars = this.fStars === i ? i - 1 : i; + this.note = ''; + }) + }, (i: number): string => 'star_' + i.toString()) + Text(this.fStars > 0 ? this.fStars.toString() + ' 星' : '未评') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .margin({ left: 4 }) + } + .layoutWeight(1) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + // ---- 章节进度(只读:由阅读器翻页写,接口没有回写入口)---- + if (!this.isNew()) { + Row({ space: 8 }) { + Text('进度') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width(34) + Text(this.progressText()) + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.muted) + .layoutWeight(1) + .maxLines(2) + } + .width('100%') + .alignItems(VerticalAlign.Top) + } + + // ---- 读书笔记(只有详情态;这里是"点进去"能看见的正事)---- + if (!this.isNew()) { + Divider() + .color(FbColors.line) + .strokeWidth(0.5) + + Row({ space: 6 }) { + Text('读书笔记') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(this.notes.length.toString() + ' 条') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + Blank() + } + .width('100%') + .alignItems(VerticalAlign.Center) + + if (this.notesLoading && this.notes.length === 0) { + Text('正在取笔记…') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width('100%') + } else if (this.notesNote !== '') { + Text(this.notesNote) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.red) + .width('100%') + .maxLines(3) + } else if (this.notes.length === 0) { + Text('这本书还没记过笔记。下面写一句就能存下来。') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width('100%') + .maxLines(3) + } else { + Column({ space: 8 }) { + ForEach(this.notes, (it: FbBookNoteItem) => { + this.noteRow(it) + }, (it: FbBookNoteItem): string => 'bn_' + it.id.toString()) + } + .width('100%') + } + + // ---- 记一条 ---- + Row({ space: 8 }) { + ForEach(FB_NOTE_KINDS, (k: string, idx: number) => { + Text(FB_NOTE_KIND_LABEL[idx]) + .fontSize(FbMetric.fontTiny) + .fontColor(this.draftKind === k ? '#FFFFFF' : FbColors.muted) + .padding({ left: 10, right: 10, top: 5, bottom: 5 }) + .borderRadius(FbMetric.radiusS) + .backgroundColor(this.draftKind === k ? fbAlpha(this.accent, 0.95) : FbColors.inputBg) + .onClick((): void => { + this.draftKind = k; + }) + }, (k: string): string => 'nk_' + k) + Blank() + } + .width('100%') + .alignItems(VerticalAlign.Center) + + TextArea({ text: this.draft, placeholder: '读到哪句、想到什么…' }) + .placeholderColor(FbColors.faint) + .placeholderFont({ size: FbMetric.fontTiny }) + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.ink) + .backgroundColor(FbColors.inputBg) + .borderRadius(FbMetric.radiusS) + .height(76) + .width('100%') + .padding({ left: 10, right: 10, top: 8, bottom: 8 }) + .onChange((v: string): void => { + this.draft = v; + this.notesNote = ''; + }) + + Text(this.storing ? '记着呢…' : '记下来') + .fontSize(FbMetric.fontSmall) + .fontWeight(FontWeight.Medium) + .fontColor(this.draft.trim() === '' || this.storing ? FbColors.faint : this.accent) + .textAlign(TextAlign.Center) + .width('100%') + .height(38) + .borderRadius(FbMetric.radiusS) + .backgroundColor(FbColors.inputBg) + .onClick((): void => { + this.storeNote(); + }) + + Text('上传电子书 / 导入笔记 / 微信读书同步在网页端「读书」里做 —— App 这边只记笔记。') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width('100%') + .maxLines(3) + } + } + .width('100%') + .alignItems(HorizontalAlign.Start) + } + .constraintSize({ maxHeight: 400 }) + .width('100%') + .scrollBar(BarState.Off) + .align(Alignment.TopStart) + + if (this.note !== '') { + Text(this.note) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.red) + .width('100%') + .maxLines(3) + } + + // ---- 底部:删除(红底白字,二次确认)+ 保存 ---- + Row({ space: 10 }) { + if (!this.isNew()) { + Text(this.confirmingDelete ? '真的删掉?' : '删除') + .fontSize(FbMetric.fontBody) + .fontWeight(FontWeight.Medium) + .fontColor('#FFFFFF') + .textAlign(TextAlign.Center) + .height(44) + .padding({ left: 16, right: 16 }) + .borderRadius(FbMetric.radiusS) + .backgroundColor(this.confirmingDelete ? '#C0533F' : fbAlpha('#C0533F', 0.86)) + .onClick((): void => { + if (this.confirmingDelete) { + this.doDelete(); + } else { + this.confirmingDelete = true; + } + }) + } + Text(this.saving ? '保存中…' : (this.isNew() ? '加进来' : '保存修改')) + .fontSize(FbMetric.fontBody) + .fontWeight(FontWeight.Medium) + .fontColor(this.saving ? FbColors.faint : '#FFFFFF') + .textAlign(TextAlign.Center) + .layoutWeight(1) + .height(44) + .borderRadius(FbMetric.radiusS) + .backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98)) + .onClick((): void => { + this.save(); + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + .width('100%') + .padding({ left: 18, right: 18, top: 16, bottom: 16 }) + .alignItems(HorizontalAlign.Start) + } + } + .width('92%') + } + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.Transparent) + } + + /** 副标题:新建说"挂在你自己名下",详情说作者 + 状态 */ + private subtitleOf(): string { + if (this.isNew()) { + return '存进书架,之后可以补评分和笔记'; + } + const who: string = this.fAuthor !== '' ? this.fAuthor : '作者不详'; + return who; + } + + /** 章节进度的人话(`chapters = 0` 表示这本书没解析过章节 ⇒ 别编百分比) */ + private progressText(): string { + if (this.chapters <= 0) { + return this.fStatus === 'done' ? '读完了' : '这本书没解析过章节,进度无从算起'; + } + const pct: number = this.fStatus === 'done' ? 100 + : Math.min(100, Math.round(this.curChapter * 100 / this.chapters)); + return '读到第 ' + this.curChapter.toString() + ' 章 / 共 ' + + this.chapters.toString() + ' 章(' + pct.toString() + '%)'; + } + + /** 一行标签 + 输入框(新建时用;`isTitle` 区分书名/作者) */ + @Builder + labeledInput(label: string, placeholder: string, isTitle: boolean) { + Row({ space: 8 }) { + Text(label) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width(34) + TextInput({ text: isTitle ? this.fTitle : this.fAuthor, placeholder: placeholder }) + .placeholderColor(FbColors.faint) + .placeholderFont({ size: FbMetric.fontTiny }) + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.ink) + .backgroundColor(FbColors.inputBg) + .borderRadius(FbMetric.radiusS) + .height(42) + .layoutWeight(1) + .padding({ left: 10, right: 10 }) + .onChange((v: string): void => { + if (isTitle) { + this.fTitle = v; + } else { + this.fAuthor = v; + } + this.note = ''; + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + } + + /** + * 状态胶囊。 + * + * ⚠️ 这里**必须**在 builder 内部读 `this.fStatus`(而不是靠参数 `on` 传值): + * `@Builder` 的**按值参数不参与状态观察** —— 传 `on: boolean` 进来, + * 点完状态改了、胶囊配色不会变(v66 车辆表单踩过同一个坑)。 + */ + @Builder + statusChip(key: string, label: string) { + Text(label) + .fontSize(FbMetric.fontTiny) + .fontColor(this.fStatus === key ? '#FFFFFF' : FbColors.muted) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + .borderRadius(FbMetric.radiusS) + .backgroundColor(this.fStatus === key ? fbAlpha(this.accent, 0.95) : FbColors.inputBg) + .onClick((): void => { + this.fStatus = key; + this.note = ''; + }) + } + + /** 一条读书笔记 */ + @Builder + noteRow(it: FbBookNoteItem) { + Column({ space: 4 }) { + Row({ space: 6 }) { + Text(it.kind === 'quote' ? '划线' : (it.kind === 'review' ? '书评' : '想法')) + .fontSize(10) + .fontColor(this.accent) + .padding({ left: 6, right: 6, top: 2, bottom: 2 }) + .borderRadius(4) + .backgroundColor(fbAlpha(this.accent, 0.12)) + if (it.personName !== '') { + Text(it.personName) + .fontSize(10) + .fontColor(FbColors.faint) + } + Blank() + if (it.createdAt !== '') { + Text(it.createdAt) + .fontSize(10) + .fontColor(FbColors.faint) + } + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Text(it.content) + .fontSize(FbMetric.fontSmall) + .fontColor(FbColors.ink) + .width('100%') + .maxLines(4) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .width('100%') + .padding(10) + .borderRadius(FbMetric.radiusS) + .backgroundColor(FbColors.surfaceAlt) + .alignItems(HorizontalAlign.Start) + } + + /** 正在提交笔记 */ + private async loadNotes(): Promise { + if (this.notesLoading) { + return; + } + this.notesLoading = true; + this.notesNote = ''; + const res: FbApiResult = await fetchBookNotes(FB_TOKEN_STORE.get(), this.editingId); + this.notesLoading = false; + if (!res.ok) { + // 笔记取不到不该拦着"改状态/评分",所以只在这里写一行提示,不往外抛 + this.notesNote = res.error !== '' ? res.error : '笔记没取到。'; + return; + } + this.notes = parseBookNotes(res.data); + } + + private async storeNote(): Promise { + if (this.storing) { + return; + } + const txt: string = this.draft.trim(); + if (txt === '') { + this.notesNote = '先写一句话再记。'; + return; + } + this.storing = true; + this.notesNote = ''; + const res: FbApiResult = await addBookNote(FB_TOKEN_STORE.get(), this.editingId, txt, this.draftKind); + this.storing = false; + if (!res.ok) { + this.notesNote = res.error !== '' ? res.error : '没记上,稍后再试。'; + return; + } + this.draft = ''; + await this.loadNotes(); + } + + private async save(): Promise { + if (this.saving) { + return; + } + const title: string = this.fTitle.trim(); + if (this.isNew()) { + if (title === '') { + this.note = '书总得有个名字。'; + return; + } + this.saving = true; + this.note = ''; + const res: FbApiResult = await createBook(FB_TOKEN_STORE.get(), title, this.fAuthor.trim(), + this.fStatus); + this.saving = false; + if (!res.ok) { + this.note = res.error !== '' ? res.error : '没加进去,稍后再试。'; + return; + } + this.onSaved(); + return; + } + this.saving = true; + this.note = ''; + /* 服务端 `rating` 是 0–10;端上是 0–5 星 ⇒ 这里 ×2 */ + const rating: number = Math.round(this.fStars * 2); + const res2: FbApiResult = await updateBook(FB_TOKEN_STORE.get(), this.editingId, this.fStatus, rating); + this.saving = false; + if (!res2.ok) { + this.note = res2.error !== '' ? res2.error : '没存上,稍后再试。'; + return; + } + this.onSaved(); + } + + private async doDelete(): Promise { + if (this.saving) { + return; + } + this.saving = true; + this.note = ''; + const res: FbApiResult = await deleteBook(FB_TOKEN_STORE.get(), this.editingId); + this.saving = false; + this.confirmingDelete = false; + if (!res.ok) { + this.note = res.error !== '' ? res.error : '没删掉,稍后再试。'; + return; + } + this.onDeleted(); + } +} + +/** + * 读书统计(端上直接按书架数据算,不需要服务端新接口)。 + * + * 🔴 用户 2026-10-20:「设置也没有任何的东西」—— 这条原来 `action` 是空串。 + * ⚠️ 只做**能算准**的几个数:本数、在读、读完、搁置、笔记条数。 + * "今年读完几本"要 `finished_at`(接口有返回但 `FbBookItem` 没收), + * "读了多少页"要 `pages` 且有值才准 —— 没做,别拿估算数字糊弄。 + */ +@ComponentV2 +export struct FbBookStatsSheet { + @Param accent: string = '#6B5B95'; + @Param items: FbBookItem[] = []; + @Event onClose: () => void = (): void => {}; + + build() { + Stack({ alignContent: Alignment.Center }) { + Column() + .width('100%') + .height('100%') + .backgroundColor(FB_MASK) + .onClick((): void => { + this.onClose(); + }) + + Column() { + FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) { + Column({ space: 14 }) { + Row({ space: 8 }) { + Text('读书统计') + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + .layoutWeight(1) + FbSheetClose({ + onClose: (): void => { + this.onClose(); + }, + color: this.accent + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row({ space: 10 }) { + this.statCell('藏书', this.countOf('').toString() + ' 本') + this.statCell('在读', this.countOf('reading').toString() + ' 本') + this.statCell('读完', this.countOf('done').toString() + ' 本') + } + .width('100%') + + Row({ space: 10 }) { + this.statCell('想读', this.countOf('wish').toString() + ' 本') + this.statCell('搁置', this.countOf('paused').toString() + ' 本') + this.statCell('笔记', this.noteTotal().toString() + ' 条') + } + .width('100%') + + Text('页数 / 字数 / 每年读了几本要去网页端「读书 → 统计」看 —— App 这边按书架现算。') + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + .width('100%') + .maxLines(3) + } + .width('100%') + .padding({ left: 18, right: 18, top: 16, bottom: 16 }) + .alignItems(HorizontalAlign.Start) + } + } + .width('92%') + } + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.Transparent) + } + + @Builder + statCell(label: string, value: string) { + Column({ space: 4 }) { + Text(value) + .fontSize(FbMetric.fontTitle) + .fontWeight(FontWeight.Medium) + .fontColor(FbColors.ink) + Text(label) + .fontSize(FbMetric.fontTiny) + .fontColor(FbColors.faint) + } + .layoutWeight(1) + .padding({ top: 12, bottom: 12 }) + .borderRadius(FbMetric.radiusS) + .backgroundColor(FbColors.surfaceAlt) + .alignItems(HorizontalAlign.Center) + } + + /** `status === ''` = 全部 */ + private countOf(status: string): number { + if (status === '') { + return this.items.length; + } + return this.items.filter((b: FbBookItem): boolean => b.status === status).length; + } + + private noteTotal(): number { + let n: number = 0; + for (const b of this.items) { + n = n + b.noteCount; + } + return n; + } +} diff --git a/entry/src/main/ets/data/FbBookRepo.ets b/entry/src/main/ets/data/FbBookRepo.ets new file mode 100644 index 0000000..874006c --- /dev/null +++ b/entry/src/main/ets/data/FbBookRepo.ets @@ -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/ 一本 + * PATCH /api/v1/books/ 改(**只认 status / rating / current_page 三个键**) + * DELETE /api/v1/books/ 删(连带封面与文件) + * GET /api/v1/books//notes 这本书的读书笔记(划线 / 想法 / 书评) + * POST /api/v1/books//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 { + /* ⚠️ `arkts-no-untyped-obj-literals`:请求体必须 `Record = {}` 逐键赋值 */ + const body: Record = {}; + 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 { + const body: Record = {}; + 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 { + 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 { + 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 { + const body: Record = {}; + 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); +} diff --git a/entry/src/main/ets/data/FbParse.ets b/entry/src/main/ets/data/FbParse.ets index 85aa861..3e1496a 100644 --- a/entry/src/main/ets/data/FbParse.ets +++ b/entry/src/main/ets/data/FbParse.ets @@ -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(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(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 | null = asObject(json); diff --git a/entry/src/main/ets/model/DemoData.ets b/entry/src/main/ets/model/DemoData.ets index 997b2a5..6a8fc28 100644 --- a/entry/src/main/ets/model/DemoData.ets +++ b/entry/src/main/ets/model/DemoData.ets @@ -298,13 +298,16 @@ export const FB_DEMO_RESOURCES: FbResourceItem[] = [ // 读书 // ============================================================ +/* ⚠️ 状态用**服务端枚举**(`wish`),别写 `want` —— 见 `fbGroupBook` 上方的说明。 + 示例数据目前没有引用处(书架只吃接口/缓存),但它必须能编译、且不能把错的 + 取值示范给后来的人看。 */ export const FB_DEMO_BOOKS: FbBookItem[] = [ - { id: 1, title: '置身事内', author: '兰小欢', status: 'reading', rate: 5, noteCount: 12, progress: 62 }, - { id: 2, title: '地球的红飘带', author: '魏巍', status: 'reading', rate: 4, noteCount: 5, progress: 28 }, - { id: 3, title: '蛤蟆先生去看心理医生', author: '罗伯特·戴博德', status: 'done', rate: 5, noteCount: 9, progress: 100 }, - { id: 4, title: '一日三秋', author: '刘震云', status: 'done', rate: 4, noteCount: 7, progress: 100 }, - { id: 5, title: '显微镜下的大明', author: '马伯庸', status: 'done', rate: 5, noteCount: 15, progress: 100 }, - { id: 6, title: '也许你该找个人聊聊', author: '洛莉·戈特利布', status: 'want', rate: 0, noteCount: 0, progress: 0 } + { id: 1, title: '置身事内', author: '兰小欢', status: 'reading', rate: 5, noteCount: 12, progress: 62, curChapter: 31, chapters: 50 }, + { id: 2, title: '地球的红飘带', author: '魏巍', status: 'reading', rate: 4, noteCount: 5, progress: 28, curChapter: 7, chapters: 25 }, + { id: 3, title: '蛤蟆先生去看心理医生', author: '罗伯特·戴博德', status: 'done', rate: 5, noteCount: 9, progress: 100, curChapter: 16, chapters: 16 }, + { id: 4, title: '一日三秋', author: '刘震云', status: 'done', rate: 4, noteCount: 7, progress: 100, curChapter: 24, chapters: 24 }, + { id: 5, title: '显微镜下的大明', author: '马伯庸', status: 'done', rate: 5, noteCount: 15, progress: 100, curChapter: 12, chapters: 12 }, + { id: 6, title: '也许你该找个人聊聊', author: '洛莉·戈特利布', status: 'wish', rate: 0, noteCount: 0, progress: 0, curChapter: 0, chapters: 0 } ]; // ============================================================ @@ -392,10 +395,19 @@ export function fbGroupResource(list: FbResourceItem[]): FbGroup return fbGroupBy(list, (it: FbResourceItem): string => it.ownerName); } -/** 按 status 分组建书架(在读 / 想读 / 读完) */ +/** + * 按 status 分组建书架(在读 / 想读 / 搁置 / 读完) + * + * 🔴🔴 v67:这四个键**必须**和服务端 `Book::STATUS` 一模一样 + * (`wish / reading / done / paused`)。原来写的是 `want`: + * · 服务端不认 `want`,落库时被 `Book::status()` 静默改成 `wish`; + * · 这里又没列 `wish` ⇒ **"想读"的书在书架里整组消失**(页头还数着它)。 + * `paused`(搁置)同理,原来也没列 ⇒ 搁置的书同样人间蒸发。 + * ⚠️ 顺序按"手头正在看的排最前",与服务端 `ORDER BY FIELD(status, …)` 一致。 + */ export function fbGroupBook(list: FbBookItem[]): FbGroup[] { - const order: string[] = ['reading', 'want', 'done']; - const titles: string[] = ['在读', '想读', '读完']; + const order: string[] = ['reading', 'wish', 'paused', 'done']; + const titles: string[] = ['在读', '想读', '搁置', '读完']; const groups: FbGroup[] = []; for (let i = 0; i < order.length; i++) { const key: string = order[i]; diff --git a/entry/src/main/ets/model/FbSettingsMenus.ets b/entry/src/main/ets/model/FbSettingsMenus.ets index b5f3d31..5336eb0 100644 --- a/entry/src/main/ets/model/FbSettingsMenus.ets +++ b/entry/src/main/ets/model/FbSettingsMenus.ets @@ -34,11 +34,20 @@ export const FB_SET_MOVIES: FbSettingItem[] = [ { icon: 'notes', label: '年度报告', hint: '这一年你看了些什么', action: 'moviereport' } ]; -/** 读书(templates/books/_settings_menu.php) */ +/** + * 读书(templates/books/_settings_menu.php) + * + * 🔴 v67(2026-10-20 用户「设置也没有任何的东西」):原来三条**全是空 `action`** + * ⇒ 整张菜单画成灰色、点哪条都没反应。 + * · `bookstats` → 端上按书架现算(藏书/在读/读完/想读/搁置/笔记条数),**能进**; + * · 另两条要**文件选择**(导入读书笔记要读 .md/.txt)与**微信读书授权**, + * 移动端没有这条链路(`BookNoteImport` / `BookParser` 只被网页调用) + * ⇒ 仍然是「暂未开放」,但 hint 写明白"去网页端",别让人以为按钮坏了。 + */ export const FB_SET_BOOKS: FbSettingItem[] = [ - { icon: 'chart', label: '读书统计', hint: '每年读了多少、多少页', action: '' }, - { icon: 'plus', label: '导入读书笔记', hint: '从笔记文件批量导入', action: '' }, - { icon: 'share', label: '微信读书导入', hint: '同步微信读书的书单', action: '' } + { icon: 'chart', label: '读书统计', hint: '藏书 / 在读 / 读完 / 笔记条数', action: 'bookstats' }, + { icon: 'plus', label: '导入读书笔记', hint: '要在网页端选文件', action: '' }, + { icon: 'share', label: '微信读书导入', hint: '要在网页端授权同步', action: '' } ]; /** 纪念日(templates/days/_settings_menu.php) diff --git a/entry/src/main/ets/model/Types.ets b/entry/src/main/ets/model/Types.ets index ec4c9ea..ff081df 100644 --- a/entry/src/main/ets/model/Types.ets +++ b/entry/src/main/ets/model/Types.ets @@ -756,19 +756,52 @@ export interface FbResourceItem { // 读书(books) // ============================================================ -/** 一本书(books/index.php 书架) */ +/** + * 一本书(books/index.php 书架) + * + * 🔴🔴 v67(2026-10-20)——把**三个和服务端口径不一致**的字段一次捋对, + * 这三个都是「不报错、不崩溃,但显示的东西是错的」: + * + * ① `status`:服务端 `Book::STATUS` 只有 `wish / reading / done / paused`, + * **没有 `want`**。而 `Book::status()` 遇到不认识的值会**静默改成 `wish`**, + * 端上分组又只认 `want` ⇒ 线上的"想读"的书在书架里**一本都不显示** + * (页头数字还数着它)—— 这正是"读书列表不对"的来源。 + * ② `rate`:服务端 `rating` 是 **0–10**,端上一直当 0–5 用 ⇒ 8 分的书显示成 `8.0/5`。 + * ③ `progress`:服务端 `progress` 是**读到第几章**(`Book::setProgress` 的注释写着 + * 「读到第几章」),不是百分比 ⇒ 读到第 30 章的书,进度条永远停在 30%。 + */ export interface FbBookItem { id: number; title: string; author: string; - /** 状态:want(想读)/ reading(在读)/ done(读完) */ + /** 状态:`wish` 想读 / `reading` 在读 / `done` 读完 / `paused` 搁置(**必须以服务端枚举为准**) */ status: string; - /** 评分 0–5(0 表示未评) */ + /** 评分 0–5(0 表示未评)。服务端存 0–10,解析时折半、写回时乘 2 */ rate: number; /** 笔记条数 */ noteCount: number; - /** 阅读进度(0–100,done 时为 100) */ + /** 阅读进度(0–100;读完恒 100,没解析过章节的书为 0) */ progress: number; + /** 读到第几章(服务端 `progress` 原值,0 = 还没开始) */ + curChapter: number; + /** 共几章(服务端 `chapter_count`,0 = 这本书没解析过章节 ⇒ 进度无从谈起) */ + chapters: number; +} + +/** 一条读书笔记(`book_notes` 表:划线 / 想法 / 书评) */ +export interface FbBookNoteItem { + id: number; + /** quote 划线 / note 想法 / review 书评 */ + kind: string; + content: string; + /** 章节(导入时带得上的话) */ + chapter: string; + /** 位置(页数 / 百分比 / 微信读书的定位串) */ + loc: string; + /** 谁写的(显示名,可能为空 = 导入时不知道是谁) */ + personName: string; + /** 记下的时间(只取日期) */ + createdAt: string; } // ============================================================ diff --git a/entry/src/main/ets/pages/books/FbBooksPage.ets b/entry/src/main/ets/pages/books/FbBooksPage.ets index 822e04f..8b89a29 100644 --- a/entry/src/main/ets/pages/books/FbBooksPage.ets +++ b/entry/src/main/ets/pages/books/FbBooksPage.ets @@ -2,10 +2,17 @@ * 家书 FamilyBook · 读书模块(鸿蒙端) * * 对应 Web 端 `templates/books/index.php`(书架 + 上传解析 + 在线读 + 笔记 + 统计)。 - * 端上先做**书架**这一层:按状态分组(在读 / 想读 / 读完), - * 每本书显示作者、评分、笔记数、阅读进度。 + * 端上做**书架 + 一本书的详情**这两层: + * · 书架按状态分组(在读 / 想读 / 搁置 / 读完),显示作者、评分、笔记数、进度; + * · 点一本书 → `FbBookSheet`(状态 / 评分 / 章节进度 / 读书笔记 / 删除); + * · 页头 + → 同一浮层的新建态(书名 / 作者 / 状态); + * · 设置菜单 → 读书统计(端上现算)+ 两条网页端能力的入口说明。 * - * 「在线读」「上传 EPUB 解析」「笔记导入」这些属于交互+文件能力,留给第二轮。 + * 🔴 v67(用户 2026-10-20「其他的功能也逐步完成……读书点不进去」): + * 原来这里**只有书架**:书行没有 `onClick`、页头"上传/统计"按钮没有 `onTap`、 + * 设置三项全是空 `action`。三处现在都接上了。 + * + * 「在线读」「上传 EPUB 解析」「笔记导入」属于交互+文件能力,仍在网页端。 */ import { FbColors, FbMetric, fbTint, FbTint, FB_MASK } from '../../common/Theme'; @@ -19,6 +26,7 @@ import { FB_DOCK_RESERVE } from '../../components/FloatDock'; import { fbOnScroll, fbOnTouch } from '../../common/FbScroll'; import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim'; import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet'; +import { FbBookSheet, FbBookStatsSheet } from '../../components/FbBookSheet'; import { FB_BACK } from '../../common/FbBack'; import { FB_SET_BOOKS } from '../../model/FbSettingsMenus'; @@ -45,6 +53,20 @@ export struct FbBooksPage { @Local all: FbBookItem[] = []; + // ---- 一本书的浮层(v67:原来点了没反应)---- + @Local bookOpen: boolean = false; + /** 0 = 加一本;>0 = 这本书的详情 */ + @Local bookId: number = 0; + @Local bookTitle: string = ''; + @Local bookAuthor: string = ''; + @Local bookStatus: string = 'wish'; + @Local bookRate: number = 0; + @Local bookCur: number = 0; + @Local bookChapters: number = 0; + + /** 读书统计浮层 */ + @Local statsOpen: boolean = false; + /** * 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。 * @@ -61,6 +83,14 @@ export struct FbBooksPage { } private backOnce(): boolean { + if (this.bookOpen) { + this.bookOpen = false; + return true; + } + if (this.statsOpen) { + this.statsOpen = false; + return true; + } if (this.settingsOpen) { this.settingsOpen = false; return true; @@ -95,7 +125,13 @@ export struct FbBooksPage { */ @Monitor('settingsOpen') onSettingsChanged(): void { - fbAppState().sheetCount = this.settingsOpen ? 1 : 0; + fbAppState().sheetCount = (this.bookOpen || this.statsOpen || this.settingsOpen) ? 1 : 0; + } + + /** 三个浮层:任一个开着都要让底部悬浮栏让位(它们各自带全屏遮罩,会盖住悬浮栏) */ + @Monitor('bookOpen', 'statsOpen') + onSheetsChanged(): void { + fbAppState().sheetCount = (this.bookOpen || this.statsOpen || this.settingsOpen) ? 1 : 0; } build() { @@ -120,8 +156,22 @@ export struct FbBooksPage { .alignItems(HorizontalAlign.Start) .layoutWeight(1) - // 上传 / 统计(Web 端在设置菜单里) - FbIconButton({ icon: $r('app.media.fb_ic_chart'), tint: FbTint.muted }) + /* 🔴 v67:原来这颗是「统计」图标且**没有 `onTap`**(纯摆设,点了没反应)。 + 现在拆成两颗:+ 加一本(主操作)、统计(设置里也有一条,这里给个快捷)。 */ + FbIconButton({ + icon: $r('app.media.fb_ic_plus'), + tint: this.accent, + onTap: (): void => { + this.openNewBook(); + } + }) + FbIconButton({ + icon: $r('app.media.fb_ic_chart'), + tint: FbTint.muted, + onTap: (): void => { + this.statsOpen = true; + } + }) // 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16。 // 点开是本模块的设置菜单(条目照网页版 `templates/<模块>/_settings_menu.php`)。 FbIconButton({ @@ -196,8 +246,51 @@ export struct FbBooksPage { this.settingsOpen = false; }, onPick: (action: string): void => { - // 条目里 `action === ''` 的是「暂未开放」(组件里已画灰、点了不响应) - // ⇒ 这里只需处理已实现的;目前都还没有,保持浮层开着即可。 + /* 🔴 v67:三条原来是空 `action`(画灰、点了没反应 = 用户说的"设置没有任何东西")。 + 现在 `bookstats` 真的能进去;另两条要文件选择/微信授权, + 网页端才有 ⇒ 在组件里仍是「暂未开放」,这里的 hint 也写清楚了。 */ + if (action === 'bookstats') { + this.settingsOpen = false; + this.statsOpen = true; + } + } + }) + .transition(fbSheetTransition()) + } + + // ---- 一本书(新建 / 详情)---- + if (this.bookOpen) { + FbBookSheet({ + accent: this.accent, + editingId: this.bookId, + initTitle: this.bookTitle, + initAuthor: this.bookAuthor, + initStatus: this.bookStatus, + initRate: this.bookRate, + curChapter: this.bookCur, + chapters: this.bookChapters, + onClose: (): void => { + this.bookOpen = false; + }, + onSaved: (): void => { + this.bookOpen = false; + this.load(); + }, + onDeleted: (): void => { + this.bookOpen = false; + this.load(); + } + }) + .transition(fbSheetTransition()) + } + + // ---- 读书统计 ---- + if (this.statsOpen) { + FbBookStatsSheet({ + accent: this.accent, + items: this.all, + onClose: (): void => { + this.statsOpen = false; } }) .transition(fbSheetTransition()) @@ -207,6 +300,30 @@ export struct FbBooksPage { .height('100%') } + /** 点一本书 → 详情 */ + private openBook(b: FbBookItem): void { + this.bookId = b.id; + this.bookTitle = b.title; + this.bookAuthor = b.author; + this.bookStatus = b.status; + this.bookRate = b.rate; + this.bookCur = b.curChapter; + this.bookChapters = b.chapters; + this.bookOpen = true; + } + + /** 页头 + → 走同一个浮层的新建态 */ + private openNewBook(): void { + this.bookId = 0; + this.bookTitle = ''; + this.bookAuthor = ''; + this.bookStatus = 'wish'; + this.bookRate = 0; + this.bookCur = 0; + this.bookChapters = 0; + this.bookOpen = true; + } + /** 一本书:书脊色块(代替封面)+ 书名/作者 + 评分/笔记/进度 */ @Builder bookRow(b: FbBookItem) { @@ -285,6 +402,10 @@ export struct FbBooksPage { .borderRadius(FbMetric.radius) .margin({ bottom: 8 }) .alignItems(VerticalAlign.Center) + /* 🔴 v67:整行可点 → 打开这本书(原来**没有 `onClick`** ⇒ "点不进去") */ + .onClick((): void => { + this.openBook(b); + }) } @Builder