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,680 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 读书浮层:一本书(新建 / 详情 / 改 / 删 / 读书笔记)+ 读书统计
|
||||
*
|
||||
* 对应网页版 `templates/books/index.php` 的 `#bookEdit` 弹框与 `books/notes.php`。
|
||||
*
|
||||
* 🔴 用户 2026-10-20:「其他的功能也逐步完成……读书点不进去,也没法加书。」
|
||||
* 端上原来只有**书架**:书行没有 `onClick`、页头没有"加一本"、
|
||||
* 设置里三条 `action` 全是空串 ⇒ 三个入口全是死的。
|
||||
*
|
||||
* ⚠️⚠️ **服务端能力边界**(照实做,不摆假入口):
|
||||
* · `PATCH /books/<id>` 只认 `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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user