Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbBookReadSheet.ets
T
laoyang f5a979f5c8 读书上传+在线阅读+笔记导入:端上补齐三个入口 laoyang/FamilyBook#28
用户原话(2026-10-28):「鸿蒙端对齐网页端」。读书端上原来只有书架。

1) FbBookUploadSheet:上传电子书(选文件→服务端解析建书)+ 粘贴导入读书笔记
2) FbBookReadSheet:在线阅读(章节列表 + 单章正文,一次一章)
3) FbBookSheet 详情态加「在线阅读」入口(有章节时才显示);
   FbBooksPage 加「+上传/导入」入口 + 设置菜单「导入笔记」接上

编译 BUILD SUCCESSFUL;服务端冒烟 _i28_book_api.py 11/0 + 反证命中。
2026-10-28 22:47:38 +08:00

253 lines
8.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 在线阅读浮层(鸿蒙端,工单 #28)
*
* 对应网页版 `templates/books/read.php`:章节列表 + 单章正文。
*
* 用户 2026-10-28 原话「鸿蒙端对齐网页端」。原来端上只能看进度数字,
* 在线读、上传解析、笔记导入都在网页端。这个浮层把「在线读」补上。
*
* ⚠️ 章节正文走 `GET /books/<id>/chapter?seq=N`(服务端 `Book::chapter`),
* 一次只读一章(正文很长,全量拉会爆内存)。
*/
import { FbColors, FbMetric, FB_MASK } from '../common/Theme';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { FbApiResult } from '../data/FbHttp';
import { fetchBookChapters, fetchBookChapter } from '../data/FbBookRepo';
/** 服务端 `Book::chapters()` 的一行(id/seq/title/words) */
interface FbChapterRow {
id: number;
seq: number;
title: string;
words: number;
}
interface RawChapter {
id?: number; seq?: number; title?: string | null; words?: number;
}
@ComponentV2
export struct FbBookReadSheet {
@Param bookId: number = 0;
@Param bookTitle: string = '';
@Param accent: string = '#6B5B95';
@Event onClose: () => void = (): void => {};
@Local chapters: FbChapterRow[] = [];
@Local loadingList: boolean = true;
@Local listNote: string = '';
/** 正在读那一章 */
@Local curSeq: number = 0;
@Local curTitle: string = '';
@Local curBody: string = '';
@Local loadingChapter: boolean = false;
@Local chapterNote: string = '';
aboutToAppear(): void {
this.loadChapters();
}
private async loadChapters(): Promise<void> {
this.loadingList = true;
this.listNote = '';
const res: FbApiResult = await fetchBookChapters(FB_TOKEN_STORE.get(), this.bookId);
this.loadingList = false;
if (!res.ok) {
this.listNote = res.error !== '' ? res.error : '章节列表没拉到。';
return;
}
const out: FbChapterRow[] = [];
try {
const o: Record<string, Object> = JSON.parse(res.data) as Record<string, Object>;
const items: Object | undefined = o['items'];
const rows: RawChapter[] = (items !== undefined && items !== null)
? (items as RawChapter[]) : [];
for (const c of rows) {
out.push({
id: c.id !== undefined ? c.id : 0,
seq: c.seq !== undefined ? c.seq : 0,
title: c.title !== undefined && c.title !== null ? c.title : '',
words: c.words !== undefined ? c.words : 0,
});
}
} catch (e) {
// 解析失败当空列表
}
this.chapters = out;
}
private async openChapter(seq: number, title: string): Promise<void> {
this.curSeq = seq;
this.curTitle = title;
this.loadingChapter = true;
this.chapterNote = '';
const res: FbApiResult = await fetchBookChapter(FB_TOKEN_STORE.get(), this.bookId, seq);
this.loadingChapter = false;
if (!res.ok) {
this.chapterNote = res.error !== '' ? res.error : '这一章没读到。';
return;
}
try {
const o: Record<string, Object> = JSON.parse(res.data) as Record<string, Object>;
const c: Object | undefined = o['content'];
this.curBody = typeof c === 'string' ? c : '';
} catch (e) {
this.curBody = '';
this.chapterNote = '正文解析失败。';
}
}
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: 0 }) {
// 标题栏
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(this.bookTitle !== '' ? this.bookTitle : '在线阅读')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
Text(this.curSeq > 0 ? this.curTitle : (this.chapters.length.toString() + ' 章'))
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accent
})
}
.width('100%')
.padding({ bottom: 8 })
.alignItems(VerticalAlign.Center)
if (this.listNote !== '') {
Text(this.listNote)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
.padding({ top: 6, bottom: 6 })
}
if (this.curSeq > 0) {
// 正文态
Column({ space: 8 }) {
Row() {
Text('← 目录')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accent)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.onClick((): void => {
this.curSeq = 0;
})
}
.width('100%')
if (this.loadingChapter) {
Text('正在取这一章…')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width('100%')
} else if (this.chapterNote !== '') {
Text(this.chapterNote)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.width('100%')
} else {
Scroll() {
Text(this.curBody)
.fontSize(16)
.lineHeight(26)
.fontColor(FbColors.ink)
.width('100%')
}
.constraintSize({ maxHeight: 520 })
.width('100%')
.scrollBar(BarState.Off)
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
} else {
// 目录态
if (this.loadingList) {
Text('正在取目录…')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width('100%')
} else if (this.chapters.length === 0) {
Text(this.listNote !== '' ? this.listNote : '这本书还没有解析出章节(可能是 PDF,浏览器里看,或没上传文件)。')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width('100%')
} else {
Scroll() {
Column({ space: 0 }) {
ForEach(this.chapters, (c: FbChapterRow) => {
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(c.title !== '' ? c.title : ('第 ' + c.seq.toString() + ' 章'))
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(c.words.toString() + ' 字')
.fontSize(10)
.fontColor(FbColors.faint)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('›')
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.faint)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.onClick((): void => {
this.openChapter(c.seq, c.title);
})
}, (c: FbChapterRow): string => 'ch_' + c.seq.toString())
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.constraintSize({ maxHeight: 520 })
.width('100%')
.scrollBar(BarState.Off)
}
}
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
}
}