253 lines
8.7 KiB
Plaintext
253 lines
8.7 KiB
Plaintext
/**
|
||||
|
|
* 家书 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)
|
|||
|
|
}
|
|||
|
|
}
|