327 lines
12 KiB
Plaintext
327 lines
12 KiB
Plaintext
/**
|
||||
|
|
* 家书 FamilyBook · 笔记浮层(新建 / 看 / 改 / 删)(鸿蒙端)
|
|||
|
|
*
|
|||
|
|
* 对应网页版 `templates/notes/`(网页是"卡片 + 详情页",端上合成一个浮层:
|
|||
|
|
* 卡片是一段短文,为它单开一屏不值当)。
|
|||
|
|
*
|
|||
|
|
* 🔴 用户 2026-10-20:「笔记现在也点不进去,记笔记也记不了。」
|
|||
|
|
* ⇒ 端上既没有新建、也没有详情:`FbPrimaryButton` 无 `onTap`、卡片无 `onClick`。
|
|||
|
|
*
|
|||
|
|
* ⚠️ 端上只支持**纯文本**(服务端 `body` → 一个 `text` 块):
|
|||
|
|
* 网页那边可以图文混排(`note_blocks` 有 image 块),端上先不做 ——
|
|||
|
|
* 图片块的内容是附件 key,硬拼进文本框会变成一串乱码。
|
|||
|
|
* 所以已有笔记里的图片块在端上显示成「[图片]」(见 `parseNotes` 的 `noteBodyOf`)。
|
|||
|
|
*
|
|||
|
|
* ⚠️ **只读**:别人共享给我的笔记能看不能改(服务端 `Note::canWrite` 那一套)。
|
|||
|
|
* 端上按 `mine` 判断 —— 保守一点宁可少给编辑,也不能给个点了报 403 的按钮。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
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 { createNote, updateNote, deleteNote } from '../data/FbNoteRepo';
|
|||
|
|
import { FbApiResult } from '../data/FbHttp';
|
|||
|
|
|
|||
|
|
@ComponentV2
|
|||
|
|
export struct FbNoteSheet {
|
|||
|
|
/** 模块主题色(笔记 = 褐灰) */
|
|||
|
|
@Param accent: string = '#7A6A4F';
|
|||
|
|
/** 正在看/改的笔记 id(0 = 新建) */
|
|||
|
|
@Param editingId: number = 0;
|
|||
|
|
@Param editingTitle: string = '';
|
|||
|
|
@Param editingBody: string = '';
|
|||
|
|
@Param editingTags: string = '';
|
|||
|
|
/** 是不是我写的(不是就只能看) */
|
|||
|
|
@Param mine: boolean = true;
|
|||
|
|
|
|||
|
|
@Event onClose: () => void = (): void => {};
|
|||
|
|
@Event onSaved: () => void = (): void => {};
|
|||
|
|
@Event onDeleted: () => void = (): void => {};
|
|||
|
|
|
|||
|
|
@Local title: string = '';
|
|||
|
|
@Local body: string = '';
|
|||
|
|
/** 标签输入(用空格或逗号分隔,服务端会自己归一) */
|
|||
|
|
@Local tags: string = '';
|
|||
|
|
@Local saving: boolean = false;
|
|||
|
|
@Local confirmingDelete: boolean = false;
|
|||
|
|
@Local note: string = '';
|
|||
|
|
|
|||
|
|
aboutToAppear(): void {
|
|||
|
|
this.title = this.editingTitle;
|
|||
|
|
this.body = this.editingBody;
|
|||
|
|
this.tags = this.editingTags;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 能不能改:新建时当然能;已有笔记只有自己写的能改 */
|
|||
|
|
private editOk(): boolean {
|
|||
|
|
return this.editingId <= 0 || this.mine;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 标签串 → 数组(逗号 / 顿号 / 空格 / # 都当分隔) */
|
|||
|
|
private tagList(): string[] {
|
|||
|
|
const norm: string = this.tags.replace(new RegExp('#', 'g'), ' ')
|
|||
|
|
.replace(new RegExp(',', 'g'), ' ')
|
|||
|
|
.replace(new RegExp(',', 'g'), ' ')
|
|||
|
|
.replace(new RegExp('、', 'g'), ' ');
|
|||
|
|
const parts: string[] = norm.split(' ');
|
|||
|
|
const out: string[] = [];
|
|||
|
|
for (const p of parts) {
|
|||
|
|
const t: string = p.trim();
|
|||
|
|
if (t !== '' && out.indexOf(t) < 0) {
|
|||
|
|
out.push(t);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
build() {
|
|||
|
|
/* 🔴🔴 根必须 `Stack`:遮罩与卡片要"叠",不能当 `Column` 子项"排"
|
|||
|
|
(排会把卡片挤出屏幕 —— 2026-10-20 真机事故,见 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: 12 }) {
|
|||
|
|
// ---- 标题栏 ----
|
|||
|
|
Row({ space: 8 }) {
|
|||
|
|
Column({ space: 2 }) {
|
|||
|
|
Text(this.editingId > 0 ? (this.mine ? '笔记' : '别人共享的笔记') : '记一条')
|
|||
|
|
.fontSize(FbMetric.fontTitle)
|
|||
|
|
.fontWeight(FontWeight.Medium)
|
|||
|
|
.fontColor(FbColors.ink)
|
|||
|
|
.maxLines(1)
|
|||
|
|
Text(this.editOk() ? '随手记点什么,按标签归类和检索' : '只读:只能看,改要作者来')
|
|||
|
|
.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%')
|
|||
|
|
.alignItems(VerticalAlign.Center)
|
|||
|
|
|
|||
|
|
Scroll() {
|
|||
|
|
Column({ space: 12 }) {
|
|||
|
|
// ---- 标题 ----
|
|||
|
|
Row({ space: 8 }) {
|
|||
|
|
Text('标题')
|
|||
|
|
.fontSize(FbMetric.fontTiny)
|
|||
|
|
.fontColor(FbColors.faint)
|
|||
|
|
.width(34)
|
|||
|
|
TextInput({ text: this.title, 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 })
|
|||
|
|
.enabled(this.editOk())
|
|||
|
|
.onChange((v: string): void => {
|
|||
|
|
this.title = v;
|
|||
|
|
this.note = '';
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.alignItems(VerticalAlign.Center)
|
|||
|
|
|
|||
|
|
// ---- 正文 ----
|
|||
|
|
Row({ space: 8 }) {
|
|||
|
|
Text('正文')
|
|||
|
|
.fontSize(FbMetric.fontTiny)
|
|||
|
|
.fontColor(FbColors.faint)
|
|||
|
|
.width(34)
|
|||
|
|
TextArea({ text: this.body, placeholder: '想记的都写在这儿' })
|
|||
|
|
.placeholderColor(FbColors.faint)
|
|||
|
|
.placeholderFont({ size: FbMetric.fontTiny })
|
|||
|
|
.fontSize(FbMetric.fontSmall)
|
|||
|
|
.fontColor(FbColors.ink)
|
|||
|
|
.backgroundColor(FbColors.inputBg)
|
|||
|
|
.borderRadius(FbMetric.radiusS)
|
|||
|
|
.height(200)
|
|||
|
|
.layoutWeight(1)
|
|||
|
|
.padding({ left: 10, right: 10, top: 8, bottom: 8 })
|
|||
|
|
.enabled(this.editOk())
|
|||
|
|
.onChange((v: string): void => {
|
|||
|
|
this.body = v;
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.alignItems(VerticalAlign.Top)
|
|||
|
|
|
|||
|
|
// ---- 标签 ----
|
|||
|
|
Row({ space: 8 }) {
|
|||
|
|
Text('标签')
|
|||
|
|
.fontSize(FbMetric.fontTiny)
|
|||
|
|
.fontColor(FbColors.faint)
|
|||
|
|
.width(34)
|
|||
|
|
TextInput({ text: this.tags, 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 })
|
|||
|
|
.enabled(this.editOk())
|
|||
|
|
.onChange((v: string): void => {
|
|||
|
|
this.tags = v;
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.alignItems(VerticalAlign.Center)
|
|||
|
|
|
|||
|
|
// 图片块在端上只能显示成「[图片]」—— 说清楚,别让人以为内容丢了
|
|||
|
|
if (this.editingId > 0 && this.body.indexOf('[图片]') >= 0) {
|
|||
|
|
Text('这条笔记里的图片在网页端看;App 这边先显示成「[图片]」。')
|
|||
|
|
.fontSize(FbMetric.fontTiny)
|
|||
|
|
.fontColor(FbColors.faint)
|
|||
|
|
.width('100%')
|
|||
|
|
.maxLines(2)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.alignItems(HorizontalAlign.Start)
|
|||
|
|
}
|
|||
|
|
.constraintSize({ maxHeight: 420 })
|
|||
|
|
.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.editingId > 0 && this.mine) {
|
|||
|
|
Text(this.confirmingDelete ? '真的删掉?' : '删除')
|
|||
|
|
.fontSize(FbMetric.fontBody)
|
|||
|
|
.fontWeight(FontWeight.Medium)
|
|||
|
|
.fontColor('#FFFFFF')
|
|||
|
|
.textAlign(TextAlign.Center)
|
|||
|
|
.height(44)
|
|||
|
|
.padding({ left: 16, right: 16 })
|
|||
|
|
.borderRadius(FbMetric.radiusS)
|
|||
|
|
/* 🔴 v61:删除按钮统一红底白字。红值取自 color.json 的 `red` */
|
|||
|
|
.backgroundColor(this.confirmingDelete ? '#C0533F' : fbAlpha('#C0533F', 0.86))
|
|||
|
|
.onClick((): void => {
|
|||
|
|
if (this.confirmingDelete) {
|
|||
|
|
this.doDelete();
|
|||
|
|
} else {
|
|||
|
|
this.confirmingDelete = true;
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
if (this.editOk()) {
|
|||
|
|
Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '记下来'))
|
|||
|
|
.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();
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
Text('知道了')
|
|||
|
|
.fontSize(FbMetric.fontBody)
|
|||
|
|
.fontWeight(FontWeight.Medium)
|
|||
|
|
.fontColor('#FFFFFF')
|
|||
|
|
.textAlign(TextAlign.Center)
|
|||
|
|
.layoutWeight(1)
|
|||
|
|
.height(44)
|
|||
|
|
.borderRadius(FbMetric.radiusS)
|
|||
|
|
.backgroundColor(fbAlpha(this.accent, 0.9))
|
|||
|
|
.onClick((): void => {
|
|||
|
|
this.onClose();
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.alignItems(VerticalAlign.Center)
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
|
|||
|
|
.alignItems(HorizontalAlign.Start)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.width('92%')
|
|||
|
|
}
|
|||
|
|
.width('100%')
|
|||
|
|
.height('100%')
|
|||
|
|
.hitTestBehavior(HitTestMode.Transparent)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async save(): Promise<void> {
|
|||
|
|
if (this.saving) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const t: string = this.title.trim();
|
|||
|
|
const b: string = this.body.trim();
|
|||
|
|
if (t === '' && b === '') {
|
|||
|
|
this.note = '标题和正文至少写一样(服务端不允许存一条空笔记)。';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.saving = true;
|
|||
|
|
this.note = '';
|
|||
|
|
/* ⚠️ 标题空着也能存(服务端允许纯正文笔记),但列表卡片上没标题会很难认
|
|||
|
|
⇒ 用正文首行兜个标题(只影响显示,不改变用户输入)。 */
|
|||
|
|
const useTitle: string = t !== '' ? t : b.substring(0, 20);
|
|||
|
|
const token: string = FB_TOKEN_STORE.get();
|
|||
|
|
const res: FbApiResult = this.editingId > 0
|
|||
|
|
? await updateNote(token, this.editingId, useTitle, b, this.tagList())
|
|||
|
|
: await createNote(token, useTitle, b, this.tagList());
|
|||
|
|
this.saving = false;
|
|||
|
|
if (!res.ok) {
|
|||
|
|
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.onSaved();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async doDelete(): Promise<void> {
|
|||
|
|
if (this.saving) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.saving = true;
|
|||
|
|
this.note = '';
|
|||
|
|
const res: FbApiResult = await deleteNote(FB_TOKEN_STORE.get(), this.editingId);
|
|||
|
|
this.saving = false;
|
|||
|
|
this.confirmingDelete = false;
|
|||
|
|
if (!res.ok) {
|
|||
|
|
this.note = res.error !== '' ? res.error : '删除失败,请稍后再试。';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.onDeleted();
|
|||
|
|
}
|
|||
|
|
}
|