读书上传+在线阅读+笔记导入:端上补齐三个入口 laoyang/FamilyBook#28
用户原话(2026-10-28):「鸿蒙端对齐网页端」。读书端上原来只有书架。 1) FbBookUploadSheet:上传电子书(选文件→服务端解析建书)+ 粘贴导入读书笔记 2) FbBookReadSheet:在线阅读(章节列表 + 单章正文,一次一章) 3) FbBookSheet 详情态加「在线阅读」入口(有章节时才显示); FbBooksPage 加「+上传/导入」入口 + 设置菜单「导入笔记」接上 编译 BUILD SUCCESSFUL;服务端冒烟 _i28_book_api.py 11/0 + 反证命中。
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 家书 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)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { createBook, updateBook, deleteBook, fetchBookNotes, addBookNote } from
|
||||
import { parseBookNotes } from '../data/FbParse';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
import { FbBookItem, FbBookNoteItem } from '../model/Types';
|
||||
import { FbBookReadSheet } from './FbBookReadSheet';
|
||||
|
||||
/** 状态选项:`key` 必须与服务端 `Book::STATUS` 逐字一致 */
|
||||
const FB_BOOK_STATUS: string[] = ['wish', 'reading', 'done', 'paused'];
|
||||
@@ -75,6 +76,8 @@ export struct FbBookSheet {
|
||||
@Local draftKind: string = 'note';
|
||||
/** 正在提交笔记(与「保存这本书」分开:两个请求互不影响) */
|
||||
@Local storing: boolean = false;
|
||||
/** 在线阅读浮层是否打开(工单 #28) */
|
||||
@Local readOpen: boolean = false;
|
||||
|
||||
private isNew(): boolean {
|
||||
return this.editingId <= 0;
|
||||
@@ -200,9 +203,20 @@ export struct FbBookSheet {
|
||||
.fontColor(FbColors.muted)
|
||||
.layoutWeight(1)
|
||||
.maxLines(2)
|
||||
if (this.chapters > 0) {
|
||||
Text('在线阅读')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||
.onClick((): void => {
|
||||
this.readOpen = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
// ---- 读书笔记(只有详情态;这里是"点进去"能看见的正事)----
|
||||
@@ -361,6 +375,18 @@ export struct FbBookSheet {
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
|
||||
// ---- 在线阅读(工单 #28)----
|
||||
if (this.readOpen) {
|
||||
FbBookReadSheet({
|
||||
bookId: this.editingId,
|
||||
bookTitle: this.fTitle,
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.readOpen = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 上传电子书 + 导入读书笔记(鸿蒙端,工单 #28)
|
||||
*
|
||||
* 对应网页端 `templates/books/form.php` 的上传 + `books/notes_import.php` 的粘贴导入。
|
||||
*
|
||||
* 用户 2026-10-28 原话「鸿蒙端对齐网页端」。原来这两样都要去网页端。
|
||||
* 现在:① 选一个 EPUB/TXT 文件上传 → 服务端解析建书;② 粘贴读书笔记原文 → 解析导入。
|
||||
*
|
||||
* ⚠️ 上传用文件选择器(`DocumentPicker`),不是相册 —— 书是文档不是图。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
||||
import { FbSheetRing } from './FbSheetRing';
|
||||
import { FbSheetClose } from './FbSheetClose';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
import { uploadBook, importBookNotes } from '../data/FbBookRepo';
|
||||
import { picker } from '@kit.CoreFileKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
|
||||
const FB_TABS: string[] = ['上传电子书', '导入笔记'];
|
||||
|
||||
@ComponentV2
|
||||
export struct FbBookUploadSheet {
|
||||
@Param accent: string = '#6B5B95';
|
||||
|
||||
@Event onClose: () => void = (): void => {};
|
||||
/** 上传 / 导入成功,书架要刷新 */
|
||||
@Event onDone: () => void = (): void => {};
|
||||
|
||||
@Local tab: number = 0;
|
||||
@Local busy: boolean = false;
|
||||
@Local note: string = '';
|
||||
@Local okMsg: string = '';
|
||||
@Local importRaw: string = '';
|
||||
|
||||
private uploadNote(): string {
|
||||
return '支持 EPUB / TXT / PDF / MOBI / DOC 等,传到服务端解析书名、作者和章节正文。';
|
||||
}
|
||||
|
||||
/** 选文件 → 上传(服务端解析并建书) */
|
||||
private async pickAndUpload(): Promise<void> {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const docPicker = new picker.DocumentViewPicker();
|
||||
const opts = new picker.DocumentSelectOptions();
|
||||
opts.maxSelectNumber = 1;
|
||||
opts.fileSuffixFilters = ['epub', 'txt', 'pdf', 'mobi', 'azw3', 'doc', 'docx'];
|
||||
const result: Array<string> = await docPicker.select(opts);
|
||||
if (result.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.note = '';
|
||||
this.okMsg = '';
|
||||
const err: string = await uploadBook(FB_TOKEN_STORE.get(), result[0]);
|
||||
this.busy = false;
|
||||
if (err !== '') {
|
||||
this.note = err;
|
||||
return;
|
||||
}
|
||||
this.okMsg = '这本书进书架了。';
|
||||
this.onDone();
|
||||
} catch (e) {
|
||||
const be: BusinessError = e as BusinessError;
|
||||
hilog.warn(0x0000, 'FamilyBook', '上传电子书失败:%{public}s', JSON.stringify(be));
|
||||
this.busy = false;
|
||||
this.note = '选文件失败,请再试一次。';
|
||||
}
|
||||
}
|
||||
|
||||
/** 粘贴导入读书笔记 */
|
||||
private async doImport(): Promise<void> {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
const raw: string = this.importRaw.trim();
|
||||
if (raw === '') {
|
||||
this.note = '先粘贴要导入的读书笔记原文。';
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.note = '';
|
||||
this.okMsg = '';
|
||||
const res: FbApiResult = await importBookNotes(FB_TOKEN_STORE.get(), raw);
|
||||
this.busy = false;
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '没导入成,请稍后再试。';
|
||||
return;
|
||||
}
|
||||
let detail: string = '';
|
||||
try {
|
||||
const o: Record<string, Object> = JSON.parse(res.data) as Record<string, Object>;
|
||||
const c: Object | undefined = o['created'];
|
||||
const a: Object | undefined = o['added'];
|
||||
detail = (typeof c === 'number' ? '新建 ' + c.toString() + ' 本' : '') +
|
||||
(typeof a === 'number' ? '、' + a.toString() + ' 条笔记' : '');
|
||||
} catch (e) {
|
||||
detail = '';
|
||||
}
|
||||
this.okMsg = '导入完成' + (detail !== '' ? ':' + detail : '') + '。';
|
||||
this.importRaw = '';
|
||||
this.onDone();
|
||||
}
|
||||
|
||||
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: 12 }) {
|
||||
// 标题栏
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('加一本')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('上传电子书,或粘贴导入读书笔记')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbSheetClose({
|
||||
onClose: (): void => {
|
||||
this.onClose();
|
||||
},
|
||||
color: this.accent
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// tab 切换
|
||||
Row({ space: 8 }) {
|
||||
ForEach(FB_TABS, (t: string, idx: number) => {
|
||||
Text(t)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.tab === idx ? '#FFFFFF' : FbColors.muted)
|
||||
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.backgroundColor(this.tab === idx ? this.accent : FbColors.inputBg)
|
||||
.onClick((): void => {
|
||||
this.tab = idx;
|
||||
this.note = '';
|
||||
this.okMsg = '';
|
||||
})
|
||||
}, (t: string): string => 'btab_' + t)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
if (this.note !== '') {
|
||||
Text(this.note)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.red)
|
||||
.width('100%')
|
||||
.maxLines(4)
|
||||
}
|
||||
if (this.okMsg !== '') {
|
||||
Text(this.okMsg)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#5E7A3C')
|
||||
.width('100%')
|
||||
.maxLines(3)
|
||||
}
|
||||
|
||||
if (this.tab === 0) {
|
||||
// -------- 上传电子书 --------
|
||||
Column({ space: 10 }) {
|
||||
Text(this.uploadNote())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(4)
|
||||
Row() {
|
||||
Text(this.busy ? '正在上传解析…' : '选文件上传')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.busy ? FbColors.faint : '#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(44)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, this.busy ? 0.4 : 0.96))
|
||||
.onClick((): void => {
|
||||
this.pickAndUpload();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
} else {
|
||||
// -------- 导入笔记 --------
|
||||
Column({ space: 10 }) {
|
||||
Text('一次贴一段(微信读书 / Kindle 导出那种「书名 + 划线/想法」),服务端自动按书名归到对应书里。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(4)
|
||||
TextArea({ text: this.importRaw, placeholder: '粘贴读书笔记原文…' })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontTiny })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.height(160)
|
||||
.width('100%')
|
||||
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
|
||||
.onChange((v: string): void => {
|
||||
this.importRaw = v;
|
||||
})
|
||||
Row() {
|
||||
Text(this.busy ? '正在导入…' : '导入')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.busy ? FbColors.faint : '#FFFFFF')
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(44)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(fbAlpha(this.accent, this.busy ? 0.4 : 0.96))
|
||||
.onClick((): void => {
|
||||
this.doImport();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
.width('92%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.hitTestBehavior(HitTestMode.Transparent)
|
||||
}
|
||||
}
|
||||
@@ -13,18 +13,12 @@
|
||||
* 🔴 用户 2026-10-20:「其他的功能也逐步完成……设置也没有任何的东西。」
|
||||
* 读书页那时是**纯书架**:书行点不动、没有"加一本"、设置三项全灰。
|
||||
*
|
||||
* ⚠️⚠️ 两处**服务端能力边界**(写在这里,免得以后又摆出假入口):
|
||||
* 1. `PATCH` **不能改书名/作者** —— `books()` 只从请求体取
|
||||
* `status` / `current_page` / `rating`,其它键根本不读。
|
||||
* (改书名要等接口扩,别在界面上放个"书名"输入框然后存不进去。)
|
||||
* 2. `PATCH` 里的 `current_page` **不对应任何列** —— `books` 表只有
|
||||
* `progress`(读到第几章),而 `Book::update()` 不读 `current_page`
|
||||
* ⇒ 端上**不要**回写章节进度,只**显示**。
|
||||
* 3. 上传电子书 / EPUB 解析 / 笔记导入 / 微信读书同步都在网页端,
|
||||
* 移动端没有文件选择与解析链路(`BookParser`、`BookNoteImport` 只有网页在调)。
|
||||
* 🔴 工单 #28(2026-10-28):上传解析 / 在线阅读 / 笔记导入补齐(服务端已给
|
||||
* `upload` / `chapters` / `chapter` / `import_notes` 四个端点,模型早就有)。
|
||||
*/
|
||||
|
||||
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
||||
import { fbUploadOne } from './FbUpload';
|
||||
|
||||
/**
|
||||
* 加一本书。
|
||||
@@ -85,3 +79,31 @@ export async function addBookNote(token: string, id: number, content: string,
|
||||
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||
return await fbApi('/api/v1/books/' + id.toString() + '/notes', opts);
|
||||
}
|
||||
|
||||
/** 上传一本电子书(EPUB/TXT),服务端解析并建书 + 章节(工单 #28) */
|
||||
export async function uploadBook(token: string, uri: string): Promise<string> {
|
||||
return await fbUploadOne('/api/v1/books/upload', 'file', uri, token);
|
||||
}
|
||||
|
||||
/** 章节列表(工单 #28;`GET /books/<id>/chapters`) */
|
||||
export async function fetchBookChapters(token: string, id: number): Promise<FbApiResult> {
|
||||
const opts: FbApiOptions = { token: token, authProbe: true };
|
||||
return await fbApi('/api/v1/books/' + id.toString() + '/chapters', opts);
|
||||
}
|
||||
|
||||
/** 单章正文(工单 #28;`GET /books/<id>/chapter?seq=N`) */
|
||||
export async function fetchBookChapter(token: string, id: number, seq: number): Promise<FbApiResult> {
|
||||
const q: Record<string, string> = {};
|
||||
q['seq'] = seq.toString();
|
||||
const opts: FbApiOptions = { token: token, query: q, authProbe: true };
|
||||
return await fbApi('/api/v1/books/' + id.toString() + '/chapter', opts);
|
||||
}
|
||||
|
||||
/** 粘贴导入读书笔记(工单 #28;`POST /books/import_notes`) */
|
||||
export async function importBookNotes(token: string, raw: string): Promise<FbApiResult> {
|
||||
const body: Record<string, Object> = {};
|
||||
body['raw'] = raw;
|
||||
body['source'] = 'weread';
|
||||
const opts: FbApiOptions = { method: 'POST', token: token, body: body };
|
||||
return await fbApi('/api/v1/books/import_notes', opts);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const FB_SET_MOVIES: FbSettingItem[] = [
|
||||
*/
|
||||
export const FB_SET_BOOKS: FbSettingItem[] = [
|
||||
{ icon: 'chart', label: '读书统计', hint: '藏书 / 在读 / 读完 / 笔记条数', action: 'bookstats' },
|
||||
{ icon: 'plus', label: '导入读书笔记', hint: '要在网页端选文件', action: '' },
|
||||
{ icon: 'plus', label: '导入读书笔记', hint: '粘贴原文,自动归到对应书', action: 'bookimport' },
|
||||
{ icon: 'share', label: '微信读书导入', hint: '要在网页端授权同步', action: '' }
|
||||
];
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
||||
import { FbBookSheet, FbBookStatsSheet } from '../../components/FbBookSheet';
|
||||
import { FbBookUploadSheet } from '../../components/FbBookUploadSheet';
|
||||
import { FB_BACK } from '../../common/FbBack';
|
||||
import { FB_SET_BOOKS } from '../../model/FbSettingsMenus';
|
||||
|
||||
@@ -66,6 +67,8 @@ export struct FbBooksPage {
|
||||
|
||||
/** 读书统计浮层 */
|
||||
@Local statsOpen: boolean = false;
|
||||
/** 上传电子书 / 导入笔记浮层(工单 #28) */
|
||||
@Local uploadOpen: boolean = false;
|
||||
|
||||
/**
|
||||
* 页面出现:先读缓存、再拉接口(本地优先,见 FbModuleRepo 的说明)。
|
||||
@@ -157,10 +160,17 @@ export struct FbBooksPage {
|
||||
.layoutWeight(1)
|
||||
|
||||
/* 🔴 v67:原来这颗是「统计」图标且**没有 `onTap`**(纯摆设,点了没反应)。
|
||||
现在拆成两颗:+ 加一本(主操作)、统计(设置里也有一条,这里给个快捷)。 */
|
||||
现在拆成三颗:+ 上传/导入(工单 #28)、+手填、统计。 */
|
||||
FbIconButton({
|
||||
icon: $r('app.media.fb_ic_plus'),
|
||||
tint: this.accent,
|
||||
onTap: (): void => {
|
||||
this.uploadOpen = true;
|
||||
}
|
||||
})
|
||||
FbIconButton({
|
||||
icon: $r('app.media.fb_ic_edit'),
|
||||
tint: this.accent,
|
||||
onTap: (): void => {
|
||||
this.openNewBook();
|
||||
}
|
||||
@@ -247,11 +257,13 @@ export struct FbBooksPage {
|
||||
},
|
||||
onPick: (action: string): void => {
|
||||
/* 🔴 v67:三条原来是空 `action`(画灰、点了没反应 = 用户说的"设置没有任何东西")。
|
||||
现在 `bookstats` 真的能进去;另两条要文件选择/微信授权,
|
||||
网页端才有 ⇒ 在组件里仍是「暂未开放」,这里的 hint 也写清楚了。 */
|
||||
v95+(工单 #28):导入笔记也接上了(与上传同一个浮层,切到第二个 tab)。 */
|
||||
if (action === 'bookstats') {
|
||||
this.settingsOpen = false;
|
||||
this.statsOpen = true;
|
||||
} else if (action === 'bookimport') {
|
||||
this.settingsOpen = false;
|
||||
this.uploadOpen = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -295,6 +307,20 @@ export struct FbBooksPage {
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 上传电子书 / 导入笔记(工单 #28)----
|
||||
if (this.uploadOpen) {
|
||||
FbBookUploadSheet({
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.uploadOpen = false;
|
||||
},
|
||||
onDone: (): void => {
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
Reference in New Issue
Block a user