Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbBookReadSheet.ets
T

253 lines
8.7 KiB
Plaintext
Raw Normal View History

/**
* 家书 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)
}
}