笔记图片块:展示真图 + 插图 + blocks 保存(图文混排后半程)laoyang/FamilyBook#23
用户原话(2026-10-28):「鸿蒙端对齐网页端」,笔记端上只支持纯文本。 这半程改鸿蒙端,配合服务端 cd56a9d 补的 /notes/upload: 1) 展示:FbNoteItem 新增 blocks(FbNoteBlock[]),parseNotes 把原始块 带进来;FbNoteSheet 里 image 块真画图(fbImageSrc),不再显示「[图片]」。 2) 插图:正文区下加「+ 插图」,选图 → fbUploadKey 上传拿 key → 加入 待保存列表;每张图带「×」可删。 3) 保存(P0 修复):带图时改走 saveNoteWithBlocks —— 正文 text 块 + 图片 image 块整组提交。原来只传 body,服务端折成单个 text 块整组替换, **编辑带图笔记会把图片块静默删光**。 4) fbUploadKey 是 fbUpload 抽出的第二出口:复用同一 multipart 核心拿 key, 不复制第二份实现(本项目"两份实现漂移"栽过多次)。 编译 BUILD SUCCESSFUL;守卫 _fb_arkts_guard.js 1324/0;服务端冒烟 _i23_note_image_api.py 11/0(含 PATCH 后图片块不被删)+ 反证命中 2/2。
This commit is contained in:
@@ -7,10 +7,14 @@
|
|||||||
* 🔴 用户 2026-10-20:「笔记现在也点不进去,记笔记也记不了。」
|
* 🔴 用户 2026-10-20:「笔记现在也点不进去,记笔记也记不了。」
|
||||||
* ⇒ 端上既没有新建、也没有详情:`FbPrimaryButton` 无 `onTap`、卡片无 `onClick`。
|
* ⇒ 端上既没有新建、也没有详情:`FbPrimaryButton` 无 `onTap`、卡片无 `onClick`。
|
||||||
*
|
*
|
||||||
* ⚠️ 端上只支持**纯文本**(服务端 `body` → 一个 `text` 块):
|
* 🔴 工单 #23(用户 2026-10-28):图文混排。原来端上只支持纯文本——
|
||||||
* 网页那边可以图文混排(`note_blocks` 有 image 块),端上先不做 ——
|
* · 图片块显示成「[图片]」占位;
|
||||||
* 图片块的内容是附件 key,硬拼进文本框会变成一串乱码。
|
* · 更糟:保存时只传 `body`,服务端折成单个 text 块**整组替换**,
|
||||||
* 所以已有笔记里的图片块在端上显示成「[图片]」(见 `parseNotes` 的 `noteBodyOf`)。
|
* 编辑一条带图笔记会把图片块**静默删光**。
|
||||||
|
* 现在:
|
||||||
|
* · `editingBlocks` 把原始块(text/image)带进来,image 块真画图;
|
||||||
|
* · 「插图」按钮 → 选图 → 上传拿 key → 追加到图片列表;
|
||||||
|
* · 保存走 `saveNoteWithBlocks`(正文 text 块 + 图片 image 块整组提交)。
|
||||||
*
|
*
|
||||||
* ⚠️ **只读**:别人共享给我的笔记能看不能改(服务端 `Note::canWrite` 那一套)。
|
* ⚠️ **只读**:别人共享给我的笔记能看不能改(服务端 `Note::canWrite` 那一套)。
|
||||||
* 端上按 `mine` 判断 —— 保守一点宁可少给编辑,也不能给个点了报 403 的按钮。
|
* 端上按 `mine` 判断 —— 保守一点宁可少给编辑,也不能给个点了报 403 的按钮。
|
||||||
@@ -20,8 +24,15 @@ import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme';
|
|||||||
import { FbSheetClose } from './FbSheetClose';
|
import { FbSheetClose } from './FbSheetClose';
|
||||||
import { FbSheetRing } from './FbSheetRing';
|
import { FbSheetRing } from './FbSheetRing';
|
||||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||||
import { createNote, updateNote, deleteNote } from '../data/FbNoteRepo';
|
import { createNote, updateNote, saveNoteWithBlocks, deleteNote } from '../data/FbNoteRepo';
|
||||||
|
import { fbUploadKey } from '../data/FbUpload';
|
||||||
|
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||||
|
import { noteImageKey } from '../data/FbParse';
|
||||||
|
import { FbNoteBlock } from '../model/Types';
|
||||||
import { FbApiResult } from '../data/FbHttp';
|
import { FbApiResult } from '../data/FbHttp';
|
||||||
|
import { picker } from '@kit.CoreFileKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
|
|
||||||
@ComponentV2
|
@ComponentV2
|
||||||
export struct FbNoteSheet {
|
export struct FbNoteSheet {
|
||||||
@@ -31,6 +42,8 @@ export struct FbNoteSheet {
|
|||||||
@Param editingId: number = 0;
|
@Param editingId: number = 0;
|
||||||
@Param editingTitle: string = '';
|
@Param editingTitle: string = '';
|
||||||
@Param editingBody: string = '';
|
@Param editingBody: string = '';
|
||||||
|
/** 正文原始块(工单 #23:image 块借此真画图;为空则回到纯文本) */
|
||||||
|
@Param editingBlocks: FbNoteBlock[] = [];
|
||||||
@Param editingTags: string = '';
|
@Param editingTags: string = '';
|
||||||
/** 是不是我写的(不是就只能看) */
|
/** 是不是我写的(不是就只能看) */
|
||||||
@Param mine: boolean = true;
|
@Param mine: boolean = true;
|
||||||
@@ -41,6 +54,10 @@ export struct FbNoteSheet {
|
|||||||
|
|
||||||
@Local title: string = '';
|
@Local title: string = '';
|
||||||
@Local body: string = '';
|
@Local body: string = '';
|
||||||
|
/** 图片块 key(工单 #23:已存在的 + 新插的,按序保存) */
|
||||||
|
@Local imageKeys: string[] = [];
|
||||||
|
/** 正在上传插图 */
|
||||||
|
@Local uploadingImg: boolean = false;
|
||||||
/** 标签输入(用空格或逗号分隔,服务端会自己归一) */
|
/** 标签输入(用空格或逗号分隔,服务端会自己归一) */
|
||||||
@Local tags: string = '';
|
@Local tags: string = '';
|
||||||
@Local saving: boolean = false;
|
@Local saving: boolean = false;
|
||||||
@@ -51,6 +68,17 @@ export struct FbNoteSheet {
|
|||||||
this.title = this.editingTitle;
|
this.title = this.editingTitle;
|
||||||
this.body = this.editingBody;
|
this.body = this.editingBody;
|
||||||
this.tags = this.editingTags;
|
this.tags = this.editingTags;
|
||||||
|
// 从原始块里挑出 image 块的 key(剥 `|宽度` 后缀),作为已存在图片
|
||||||
|
const keys: string[] = [];
|
||||||
|
for (const b of this.editingBlocks) {
|
||||||
|
if (b.kind === 'image') {
|
||||||
|
const k: string = noteImageKey(b.content);
|
||||||
|
if (k !== '') {
|
||||||
|
keys.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.imageKeys = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 能不能改:新建时当然能;已有笔记只有自己写的能改 */
|
/** 能不能改:新建时当然能;已有笔记只有自己写的能改 */
|
||||||
@@ -192,13 +220,52 @@ export struct FbNoteSheet {
|
|||||||
.width('100%')
|
.width('100%')
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Center)
|
||||||
|
|
||||||
// 图片块在端上只能显示成「[图片]」—— 说清楚,别让人以为内容丢了
|
// ---- 图片(工单 #23:真画图,不显示「[图片]」占位)----
|
||||||
if (this.editingId > 0 && this.body.indexOf('[图片]') >= 0) {
|
if (this.imageKeys.length > 0) {
|
||||||
Text('这条笔记里的图片在网页端看;App 这边先显示成「[图片]」。')
|
Column({ space: 8 }) {
|
||||||
.fontSize(FbMetric.fontTiny)
|
ForEach(this.imageKeys, (key: string, idx: number) => {
|
||||||
.fontColor(FbColors.faint)
|
Row({ space: 8 }) {
|
||||||
|
Image(fbImageSrc(key, FB_TOKEN_STORE.get()))
|
||||||
|
.width(72)
|
||||||
|
.height(72)
|
||||||
|
.objectFit(ImageFit.Cover)
|
||||||
|
.borderRadius(FbMetric.radiusS)
|
||||||
|
.backgroundColor(FbColors.inputBg)
|
||||||
|
if (this.editOk()) {
|
||||||
|
Text('×')
|
||||||
|
.fontSize(18)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
.width(30)
|
||||||
|
.height(30)
|
||||||
|
.borderRadius(15)
|
||||||
|
.backgroundColor(fbAlpha('#C0533F', 0.86))
|
||||||
|
.onClick((): void => {
|
||||||
|
this.removeImage(idx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.maxLines(2)
|
.alignItems(VerticalAlign.Center)
|
||||||
|
}, (key: string, idx: number): string => 'nimg_' + idx.toString() + '_' + key)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插图按钮(只给能改的人)
|
||||||
|
if (this.editOk()) {
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text(this.uploadingImg ? '正在上传…' : '+ 插图')
|
||||||
|
.fontSize(FbMetric.fontSmall)
|
||||||
|
.fontColor(this.accent)
|
||||||
|
}
|
||||||
|
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
|
||||||
|
.borderRadius(FbMetric.radiusS)
|
||||||
|
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||||
|
.onClick((): void => {
|
||||||
|
this.pickImage();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
@@ -287,19 +354,23 @@ export struct FbNoteSheet {
|
|||||||
}
|
}
|
||||||
const t: string = this.title.trim();
|
const t: string = this.title.trim();
|
||||||
const b: string = this.body.trim();
|
const b: string = this.body.trim();
|
||||||
if (t === '' && b === '') {
|
const hasImg: boolean = this.imageKeys.length > 0;
|
||||||
this.note = '标题和正文至少写一样(服务端不允许存一条空笔记)。';
|
if (t === '' && b === '' && !hasImg) {
|
||||||
|
this.note = '标题、正文或图片至少填一样(服务端不允许存一条空笔记)。';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.note = '';
|
this.note = '';
|
||||||
/* ⚠️ 标题空着也能存(服务端允许纯正文笔记),但列表卡片上没标题会很难认
|
/* ⚠️ 标题空着也能存(服务端允许纯正文笔记),但列表卡片上没标题会很难认
|
||||||
⇒ 用正文首行兜个标题(只影响显示,不改变用户输入)。 */
|
⇒ 用正文首行兜个标题(只影响显示,不改变用户输入)。 */
|
||||||
const useTitle: string = t !== '' ? t : b.substring(0, 20);
|
const useTitle: string = t !== '' ? t : (b !== '' ? b.substring(0, 20) : '图文笔记');
|
||||||
const token: string = FB_TOKEN_STORE.get();
|
const token: string = FB_TOKEN_STORE.get();
|
||||||
const res: FbApiResult = this.editingId > 0
|
// 🔴 工单 #23:带图则走 blocks 传法,保住图片块;纯文本走老函数(等价)。
|
||||||
|
const res: FbApiResult = hasImg
|
||||||
|
? await saveNoteWithBlocks(token, this.editingId, useTitle, b, this.imageKeys, this.tagList())
|
||||||
|
: (this.editingId > 0
|
||||||
? await updateNote(token, this.editingId, useTitle, b, this.tagList())
|
? await updateNote(token, this.editingId, useTitle, b, this.tagList())
|
||||||
: await createNote(token, useTitle, b, this.tagList());
|
: await createNote(token, useTitle, b, this.tagList()));
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
||||||
@@ -308,6 +379,53 @@ export struct FbNoteSheet {
|
|||||||
this.onSaved();
|
this.onSaved();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 选一张图插入(选完立刻上传拿 key,等保存时随 blocks 一起提交) */
|
||||||
|
private async pickImage(): Promise<void> {
|
||||||
|
if (this.uploadingImg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const opts: picker.PhotoSelectOptions = new picker.PhotoSelectOptions();
|
||||||
|
opts.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
opts.maxSelectNumber = 1;
|
||||||
|
const p: picker.PhotoViewPicker = new picker.PhotoViewPicker();
|
||||||
|
const sel: picker.PhotoSelectResult = await p.select(opts);
|
||||||
|
if (sel.photoUris.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploadingImg = true;
|
||||||
|
this.note = '';
|
||||||
|
const r = await fbUploadKey('/api/v1/notes/upload', 'file', sel.photoUris[0],
|
||||||
|
FB_TOKEN_STORE.get());
|
||||||
|
this.uploadingImg = false;
|
||||||
|
if (r.error !== '') {
|
||||||
|
this.note = r.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r.key === '') {
|
||||||
|
this.note = '图片没传上去,请再试一次。';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.imageKeys = this.imageKeys.concat([r.key]);
|
||||||
|
} catch (e) {
|
||||||
|
const be: BusinessError = e as BusinessError;
|
||||||
|
hilog.warn(0x0000, 'FamilyBook', '笔记插图失败:%{public}s', JSON.stringify(be));
|
||||||
|
this.uploadingImg = false;
|
||||||
|
this.note = '选图失败,请再试一次。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从待保存列表里拿掉第 idx 张(新建时直接删;已有图片下次保存整组替换即可) */
|
||||||
|
private removeImage(idx: number): void {
|
||||||
|
const next: string[] = [];
|
||||||
|
for (let i: number = 0; i < this.imageKeys.length; i++) {
|
||||||
|
if (i !== idx) {
|
||||||
|
next.push(this.imageKeys[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.imageKeys = next;
|
||||||
|
}
|
||||||
|
|
||||||
private async doDelete(): Promise<void> {
|
private async doDelete(): Promise<void> {
|
||||||
if (this.saving) {
|
if (this.saving) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,45 +5,89 @@
|
|||||||
* POST /api/v1/notes 新建(title / body 或 blocks / tags)
|
* POST /api/v1/notes 新建(title / body 或 blocks / tags)
|
||||||
* PATCH /api/v1/notes/<id> 改
|
* PATCH /api/v1/notes/<id> 改
|
||||||
* DELETE /api/v1/notes/<id> 删(**只限作者**)
|
* DELETE /api/v1/notes/<id> 删(**只限作者**)
|
||||||
|
* POST /api/v1/notes/upload 传图片拿 key(工单 #23,草稿态,见下)
|
||||||
*
|
*
|
||||||
* 🔴 用户 2026-10-20:「笔记现在也点不进去,记笔记也记不了。」
|
* 🔴 用户 2026-10-20:「笔记现在也点不进去,记笔记也记不了。」
|
||||||
* ⇒ 端上「记一条」按钮没有 `onTap`、卡片没有 `onClick`。
|
* ⇒ 端上「记一条」按钮没有 `onTap`、卡片没有 `onClick`。
|
||||||
*
|
*
|
||||||
* ⚠️⚠️ 正文的键是 **`body`(纯文本)**,不是 `blocks`:
|
* ⚠️⚠️ 工单 #23(图文混排)**存储键从 `body` 换成 `blocks`**:
|
||||||
* `Note` 那边真正的存储是 `note_blocks` 若干块,服务端 `notes()` 收到 `body`
|
* 服务端 `Note` 真正的存储是 `note_blocks` 若干块。原来端上只传 `body`,
|
||||||
* 会包成一个 `text` 块(见 `ApiHandler::noteBlocksIn` 的说明)。
|
* 服务端会把它折成**单个 text 块**并**整组替换**(先 DELETE 再 INSERT)⇒
|
||||||
* v65 之前接口传的是 `body` 但 `Note` 只读 `blocks` —— 正文被静默丢掉;
|
* **端上编辑一条带图笔记、点保存,图片块被静默删光**(P0 数据丢失)。
|
||||||
* **现在两边对齐了**:端上传 `body`,服务端转成块。
|
* ⇒ 现在端上把"正文文本 + 图片块"拼成 `blocks` 数组一起传。
|
||||||
* ⇒ 端上不要自己去拼 `blocks`(那是网页图文混排的形态,手机上先支持纯文本)。
|
|
||||||
*
|
*
|
||||||
* ⚠️ `PATCH` 时若**不传** `body`/`blocks`,服务端会沿用库里的块(不会清空)。
|
* ⚠️ 服务端 `noteBlocksIn()` 的判定顺序:**先认 `blocks`,再认 `body`**。
|
||||||
* 这里仍然每次都传 `body`:用户看到的就是那个文本框,保存即整段替换。
|
* 所以传 `blocks` 时它整组用 `blocks`,不会再碰 `body`。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
import { fbApi, FbApiResult, FbApiOptions } from './FbHttp';
|
||||||
|
|
||||||
/** 新建笔记 */
|
/** 新建笔记(纯文本:单个 text 块) */
|
||||||
export async function createNote(token: string, title: string, body: string,
|
export async function createNote(token: string, title: string, body: string,
|
||||||
tags: string[]): Promise<FbApiResult> {
|
tags: string[]): Promise<FbApiResult> {
|
||||||
|
return saveNote(token, 0, title, body, [], tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改笔记(纯文本;🔴 仅限作者能改,但这里不判——服务端会拦) */
|
||||||
|
export async function updateNote(token: string, id: number, title: string, body: string,
|
||||||
|
tags: string[]): Promise<FbApiResult> {
|
||||||
|
return saveNote(token, id, title, body, [], tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建 / 改一条笔记(工单 #23:带图片块一起存)。
|
||||||
|
*
|
||||||
|
* @param images 图片块(每项是已上传拿回来的附件 key),排在正文后面。
|
||||||
|
*/
|
||||||
|
export async function saveNoteWithBlocks(token: string, id: number, title: string,
|
||||||
|
body: string, images: string[], tags: string[]): Promise<FbApiResult> {
|
||||||
|
return saveNote(token, id, title, body, images, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 公共落地:把正文与图片拼成 `blocks` 数组交服务端 */
|
||||||
|
async function saveNote(token: string, id: number, title: string, body: string,
|
||||||
|
images: string[], tags: string[]): Promise<FbApiResult> {
|
||||||
/* ⚠️ ArkTS 的 `arkts-no-untyped-obj-literals`:请求体必须
|
/* ⚠️ ArkTS 的 `arkts-no-untyped-obj-literals`:请求体必须
|
||||||
`Record<string, Object> = {}` 逐键赋值(见 `FbTripRepo` 同一说明)。 */
|
`Record<string, Object> = {}` 逐键赋值(见 `FbTripRepo` 同一说明)。 */
|
||||||
const payload: Record<string, Object> = {};
|
const payload: Record<string, Object> = {};
|
||||||
payload['title'] = title;
|
payload['title'] = title;
|
||||||
payload['body'] = body;
|
payload['blocks'] = buildBlocks(body, images);
|
||||||
payload['tags'] = tags;
|
payload['tags'] = tags;
|
||||||
const opts: FbApiOptions = { method: 'POST', token: token, body: payload };
|
const opts: FbApiOptions = {
|
||||||
return await fbApi('/api/v1/notes', opts);
|
method: id > 0 ? 'PATCH' : 'POST',
|
||||||
|
token: token,
|
||||||
|
body: payload
|
||||||
|
};
|
||||||
|
return await fbApi(id > 0 ? ('/api/v1/notes/' + id.toString()) : '/api/v1/notes', opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 改笔记(标题 / 正文 / 标签) */
|
/**
|
||||||
export async function updateNote(token: string, id: number, title: string, body: string,
|
* 正文文本 + 图片 key → `note_blocks` 数组。
|
||||||
tags: string[]): Promise<FbApiResult> {
|
*
|
||||||
const payload: Record<string, Object> = {};
|
* 顺序:正文一个 text 块在前,图片块依次排在后面。
|
||||||
payload['title'] = title;
|
* ⚠️ 正文/图至少一样,否则返回空数组(服务端 `cleanBlocks` 会丢空块,
|
||||||
payload['body'] = body;
|
* 而"标题和正文都空"整个请求也会被服务端拒掉)。
|
||||||
payload['tags'] = tags;
|
*/
|
||||||
const opts: FbApiOptions = { method: 'PATCH', token: token, body: payload };
|
function buildBlocks(body: string, images: string[]): Record<string, Object>[] {
|
||||||
return await fbApi('/api/v1/notes/' + id.toString(), opts);
|
const blocks: Record<string, Object>[] = [];
|
||||||
|
const txt: string = body.trim();
|
||||||
|
if (txt !== '') {
|
||||||
|
const t: Record<string, Object> = {};
|
||||||
|
t['kind'] = 'text';
|
||||||
|
t['content'] = txt;
|
||||||
|
blocks.push(t);
|
||||||
|
}
|
||||||
|
for (const key of images) {
|
||||||
|
const k: string = key.trim();
|
||||||
|
if (k === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const im: Record<string, Object> = {};
|
||||||
|
im['kind'] = 'image';
|
||||||
|
im['content'] = k;
|
||||||
|
blocks.push(im);
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删笔记(服务端只允许作者删) */
|
/** 删笔记(服务端只允许作者删) */
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import {
|
import {
|
||||||
FbAccount, FbAnniversaryItem, FbBookItem, FbBookNoteItem, FbCalEvent, FbHomeData, FbKinChip,
|
FbAccount, FbAnniversaryItem, FbBookItem, FbBookNoteItem, FbCalEvent, FbHomeData, FbKinChip,
|
||||||
FbMark,
|
FbMark,
|
||||||
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNotificationItem, FbResourceItem,
|
FbLedgerBook, FbLedgerDetail, FbLedgerEntry, FbNoteItem, FbNoteBlock, FbNotificationItem, FbResourceItem,
|
||||||
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
|
FbTaskItem, FbTripItem, FbWeather, FbStat, FbMemoryItem, FbRecentDay,
|
||||||
FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt,
|
FbTaskAttach, FbTaskComment, FbTaskDetail, FbTaskKid, FbTaskMeta, FbCatOpt, FbBoardOpt,
|
||||||
} from '../model/Types';
|
} from '../model/Types';
|
||||||
@@ -614,6 +614,7 @@ export function parseNotes(json: string): FbNoteItem[] {
|
|||||||
/* ⚠️ 服务端列表已带 `blocks` / `tags` / `excerpt`(`Note::attachChildren`),
|
/* ⚠️ 服务端列表已带 `blocks` / `tags` / `excerpt`(`Note::attachChildren`),
|
||||||
这里把它们都取出来 —— 原来只拿 blocks 算了个摘要,正文和标签全丢了。 */
|
这里把它们都取出来 —— 原来只拿 blocks 算了个摘要,正文和标签全丢了。 */
|
||||||
const body: string = noteBodyOf(r.blocks);
|
const body: string = noteBodyOf(r.blocks);
|
||||||
|
const blocks: FbNoteBlock[] = noteBlocksOf(r.blocks);
|
||||||
const tags: string[] = [];
|
const tags: string[] = [];
|
||||||
for (const t of arr<string>(r.tags)) {
|
for (const t of arr<string>(r.tags)) {
|
||||||
tags.push(s(t));
|
tags.push(s(t));
|
||||||
@@ -628,6 +629,7 @@ export function parseNotes(json: string): FbNoteItem[] {
|
|||||||
shared: n(r.is_shared) !== 0,
|
shared: n(r.is_shared) !== 0,
|
||||||
attachCount: 0,
|
attachCount: 0,
|
||||||
body: body,
|
body: body,
|
||||||
|
blocks: blocks,
|
||||||
mine: n(r.is_mine) !== 0,
|
mine: n(r.is_mine) !== 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -635,10 +637,34 @@ export function parseNotes(json: string): FbNoteItem[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `note_blocks` → 一段纯文本。
|
* `note_blocks` → 端上块数组(工单 #23:图文混排要原始块,别只留一张拼好的文本)。
|
||||||
*
|
*
|
||||||
* ⚠️ 图片块的内容是**附件 key**(不是正文),拼进来会变成一串乱码 key
|
* ⚠️ 只保留白名单里的 `text` / `image`(服务端 `Note::BLOCK_KINDS`),
|
||||||
* ⇒ 记成「[图片]」,用户一眼知道那里有张图。
|
* 其它一律归一成 text 并交给 noteBodyOf 语义处理;这里只吐给展示/编辑用。
|
||||||
|
*/
|
||||||
|
function noteBlocksOf(blocks: RawNoteBlock[] | undefined): FbNoteBlock[] {
|
||||||
|
const rows: RawNoteBlock[] = arr<RawNoteBlock>(blocks);
|
||||||
|
const out: FbNoteBlock[] = [];
|
||||||
|
for (const bk of rows) {
|
||||||
|
const kind: string = s(bk.kind);
|
||||||
|
if (kind !== 'text' && kind !== 'image') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
kind: kind,
|
||||||
|
content: s(bk.content),
|
||||||
|
position: n(bk.position),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `note_blocks` → 一段纯文本(**只拼 text 块**,图片块跳过)。
|
||||||
|
*
|
||||||
|
* 🔴 工单 #23:原来图片块被记成「[图片]」占位字,正文框显示它、保存还会把
|
||||||
|
* 它当正文写回去。现在图片块由 `FbNoteItem.blocks` 单独承载,正文只留纯文本。
|
||||||
|
* 这样:① 显示真图不靠占位字;② 保存时用 `blocks` 传法,图片不会被覆盖丢。
|
||||||
*/
|
*/
|
||||||
function noteBodyOf(blocks: RawNoteBlock[] | undefined): string {
|
function noteBodyOf(blocks: RawNoteBlock[] | undefined): string {
|
||||||
const rows: RawNoteBlock[] = arr<RawNoteBlock>(blocks);
|
const rows: RawNoteBlock[] = arr<RawNoteBlock>(blocks);
|
||||||
@@ -646,7 +672,6 @@ function noteBodyOf(blocks: RawNoteBlock[] | undefined): string {
|
|||||||
for (const b of rows) {
|
for (const b of rows) {
|
||||||
const kind: string = s(b.kind);
|
const kind: string = s(b.kind);
|
||||||
if (kind === 'image') {
|
if (kind === 'image') {
|
||||||
parts.push('[图片]');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const txt: string = s(b.content).trim();
|
const txt: string = s(b.content).trim();
|
||||||
@@ -657,6 +682,18 @@ function noteBodyOf(blocks: RawNoteBlock[] | undefined): string {
|
|||||||
return parts.join('\n\n');
|
return parts.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片块 content → 纯净附件 key(剥掉「`|宽度`」后缀)。
|
||||||
|
*
|
||||||
|
* 与 Web 端 `Note::imageKey` 同语义:图片块 content 可能是 `key` 或 `key|50`,
|
||||||
|
* 拼图片 URL 时必须取 `|` 之前的 key,否则带后缀的那张图永远加载不出来。
|
||||||
|
*/
|
||||||
|
export function noteImageKey(content: string): string {
|
||||||
|
const c: string = content.trim();
|
||||||
|
const i: number = c.indexOf('|');
|
||||||
|
return i >= 0 ? c.substring(0, i).trim() : c;
|
||||||
|
}
|
||||||
|
|
||||||
/** 通知 → FbNotificationItem[] */
|
/** 通知 → FbNotificationItem[] */
|
||||||
export function parseNotifications(json: string): FbNotificationItem[] {
|
export function parseNotifications(json: string): FbNotificationItem[] {
|
||||||
const root: Record<string, Object> | null = asObject(json);
|
const root: Record<string, Object> | null = asObject(json);
|
||||||
|
|||||||
@@ -42,15 +42,72 @@ const TAG: string = 'FbUpload';
|
|||||||
*/
|
*/
|
||||||
export async function fbUploadOne(path: string, field: string, uri: string,
|
export async function fbUploadOne(path: string, field: string, uri: string,
|
||||||
token: string): Promise<string> {
|
token: string): Promise<string> {
|
||||||
|
const r: FbUploadRaw = await fbUploadRaw(path, field, uri, token);
|
||||||
|
return r.ok ? '' : r.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片并**拿回响应载荷**(工单 #23 用)。
|
||||||
|
*
|
||||||
|
* 笔记插图要拿到服务端刚存的那张图的附件 key(存进 `image` 块),
|
||||||
|
* 而 `fbUploadOne` 只返回"成没成"、把 key 丢了。这里复用同一个
|
||||||
|
* multipart 核心(`fbUploadRaw`),成功时把信封里的 `key` 吐出来。
|
||||||
|
*
|
||||||
|
* @returns key = 附件相对键;空串且 error 非空 = 失败
|
||||||
|
*/
|
||||||
|
export interface FbUploadKey {
|
||||||
|
/** 附件 key(成功时有值) */
|
||||||
|
key: string;
|
||||||
|
/** 错误说明(空 = 成功) */
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fbUploadKey(path: string, field: string, uri: string,
|
||||||
|
token: string): Promise<FbUploadKey> {
|
||||||
|
const r: FbUploadRaw = await fbUploadRaw(path, field, uri, token);
|
||||||
|
if (!r.ok) {
|
||||||
|
return { key: '', error: r.error };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const o: Record<string, Object> = JSON.parse(r.body) as Record<string, Object>;
|
||||||
|
const k: Object | undefined = o['key'];
|
||||||
|
if (typeof k === 'string' && k !== '') {
|
||||||
|
return { key: k, error: '' };
|
||||||
|
}
|
||||||
|
// 没拿到 key:再按信封的 `data` 兜一把(不同端点习惯不同)
|
||||||
|
const d: Object | undefined = o['data'];
|
||||||
|
if (d !== undefined && typeof d === 'object' && d !== null) {
|
||||||
|
const dk: Object | undefined = (d as Record<string, Object>)['key'];
|
||||||
|
if (typeof dk === 'string' && dk !== '') {
|
||||||
|
return { key: dk, error: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { key: '', error: '服务端没回图片地址' };
|
||||||
|
} catch (err) {
|
||||||
|
return { key: '', error: '响应不是合法 JSON' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `fbUploadOne` 与 `fbUploadKey` 共用的 multipart 核心(勿复制,见文件头说明) */
|
||||||
|
interface FbUploadRaw {
|
||||||
|
ok: boolean;
|
||||||
|
/** 响应正文(成功时是非空 JSON) */
|
||||||
|
body: string;
|
||||||
|
/** 给人看的错误说明 */
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fbUploadRaw(path: string, field: string, uri: string,
|
||||||
|
token: string): Promise<FbUploadRaw> {
|
||||||
const base: string = fbApiBase();
|
const base: string = fbApiBase();
|
||||||
if (base === '') {
|
if (base === '') {
|
||||||
return '还没有设置服务器地址';
|
return { ok: false, body: '', error: '还没有设置服务器地址' };
|
||||||
}
|
}
|
||||||
if (token === '') {
|
if (token === '') {
|
||||||
return '还没登录';
|
return { ok: false, body: '', error: '还没登录' };
|
||||||
}
|
}
|
||||||
if (uri === '') {
|
if (uri === '') {
|
||||||
return '没选到文件';
|
return { ok: false, body: '', error: '没选到文件' };
|
||||||
}
|
}
|
||||||
let buf: ArrayBuffer;
|
let buf: ArrayBuffer;
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +118,7 @@ export async function fbUploadOne(path: string, field: string, uri: string,
|
|||||||
fileIo.closeSync(file);
|
fileIo.closeSync(file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hilog.warn(DOMAIN, TAG, '读文件失败:%{public}s', JSON.stringify(err));
|
hilog.warn(DOMAIN, TAG, '读文件失败:%{public}s', JSON.stringify(err));
|
||||||
return '读不出这张图';
|
return { ok: false, body: '', error: '读不出这张图' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundary: string = '----FbUpload' + Date.now().toString();
|
const boundary: string = '----FbUpload' + Date.now().toString();
|
||||||
@@ -85,7 +142,7 @@ export async function fbUploadOne(path: string, field: string, uri: string,
|
|||||||
expectDataType: http.HttpDataType.STRING
|
expectDataType: http.HttpDataType.STRING
|
||||||
});
|
});
|
||||||
if (resp.responseCode >= 200 && resp.responseCode < 300) {
|
if (resp.responseCode >= 200 && resp.responseCode < 300) {
|
||||||
return '';
|
return { ok: true, body: typeof resp.result === 'string' ? resp.result : '', error: '' };
|
||||||
}
|
}
|
||||||
const body: string = typeof resp.result === 'string' ? resp.result : '';
|
const body: string = typeof resp.result === 'string' ? resp.result : '';
|
||||||
// 服务端的错误说明是 JSON 信封里的 `error` —— 尽量把它抠出来给人话
|
// 服务端的错误说明是 JSON 信封里的 `error` —— 尽量把它抠出来给人话
|
||||||
@@ -99,11 +156,11 @@ export async function fbUploadOne(path: string, field: string, uri: string,
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 不是 JSON(如 Apache 的错误页)⇒ 保留 HTTP 码
|
// 不是 JSON(如 Apache 的错误页)⇒ 保留 HTTP 码
|
||||||
}
|
}
|
||||||
return msg;
|
return { ok: false, body: body, error: msg };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e: BusinessError = err as BusinessError;
|
const e: BusinessError = err as BusinessError;
|
||||||
hilog.warn(DOMAIN, TAG, '上传失败 %{public}s:%{public}s', path, JSON.stringify(e));
|
hilog.warn(DOMAIN, TAG, '上传失败 %{public}s:%{public}s', path, JSON.stringify(e));
|
||||||
return '网络异常';
|
return { ok: false, body: '', error: '网络异常' };
|
||||||
} finally {
|
} finally {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,27 +361,27 @@ export const FB_DEMO_NOTES: FbNoteItem[] = [
|
|||||||
{
|
{
|
||||||
id: 1, title: '阳台改造清单', excerpt: '1. 换推拉门(断桥铝,考虑隔音)2. 加一组置物架 3. 补两盆绿萝…',
|
id: 1, title: '阳台改造清单', excerpt: '1. 换推拉门(断桥铝,考虑隔音)2. 加一组置物架 3. 补两盆绿萝…',
|
||||||
tags: ['家务', '装修'], updatedAt: '10-11 09:12', shared: false, attachCount: 2,
|
tags: ['家务', '装修'], updatedAt: '10-11 09:12', shared: false, attachCount: 2,
|
||||||
body: '1. 换推拉门(断桥铝,考虑隔音)\n2. 加一组置物架\n3. 补两盆绿萝', mine: true
|
body: '1. 换推拉门(断桥铝,考虑隔音)\n2. 加一组置物架\n3. 补两盆绿萝', blocks: [], mine: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2, title: '读书摘抄 · 置身事内', excerpt: '「地方政府的行为,要从它面对的制度约束去理解。」……',
|
id: 2, title: '读书摘抄 · 置身事内', excerpt: '「地方政府的行为,要从它面对的制度约束去理解。」……',
|
||||||
tags: ['读书'], updatedAt: '10-10 22:40', shared: true, attachCount: 0,
|
tags: ['读书'], updatedAt: '10-10 22:40', shared: true, attachCount: 0,
|
||||||
body: '「地方政府的行为,要从它面对的制度约束去理解。」', mine: true
|
body: '「地方政府的行为,要从它面对的制度约束去理解。」', blocks: [], mine: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3, title: '儿子的兴趣班时间表', excerpt: '周一 16:30 数学;周三 17:00 游泳;周六 09:00 钢琴…',
|
id: 3, title: '儿子的兴趣班时间表', excerpt: '周一 16:30 数学;周三 17:00 游泳;周六 09:00 钢琴…',
|
||||||
tags: ['孩子'], updatedAt: '10-09 18:05', shared: true, attachCount: 1,
|
tags: ['孩子'], updatedAt: '10-09 18:05', shared: true, attachCount: 1,
|
||||||
body: '周一 16:30 数学\n周三 17:00 游泳\n周六 09:00 钢琴', mine: false
|
body: '周一 16:30 数学\n周三 17:00 游泳\n周六 09:00 钢琴', blocks: [], mine: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4, title: '崂山两日流水', excerpt: '第一天仰口 → 太清宫;第二天巨峰。民宿 680,两餐 218…',
|
id: 4, title: '崂山两日流水', excerpt: '第一天仰口 → 太清宫;第二天巨峰。民宿 680,两餐 218…',
|
||||||
tags: ['旅行', '记账'], updatedAt: '10-04 20:30', shared: false, attachCount: 6,
|
tags: ['旅行', '记账'], updatedAt: '10-04 20:30', shared: false, attachCount: 6,
|
||||||
body: '第一天仰口 → 太清宫;第二天巨峰。民宿 680,两餐 218', mine: true
|
body: '第一天仰口 → 太清宫;第二天巨峰。民宿 680,两餐 218', blocks: [], mine: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5, title: '车里该备的东西', excerpt: '拖车绳、搭电线、应急灯、保温毯、常用药…',
|
id: 5, title: '车里该备的东西', excerpt: '拖车绳、搭电线、应急灯、保温毯、常用药…',
|
||||||
tags: ['车'], updatedAt: '09-25 07:48', shared: false, attachCount: 0,
|
tags: ['车'], updatedAt: '09-25 07:48', shared: false, attachCount: 0,
|
||||||
body: '拖车绳、搭电线、应急灯、保温毯、常用药', mine: true
|
body: '拖车绳、搭电线、应急灯、保温毯、常用药', blocks: [], mine: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1004,18 +1004,40 @@ export interface FbNoteItem {
|
|||||||
/** 附件数 */
|
/** 附件数 */
|
||||||
attachCount: number;
|
attachCount: number;
|
||||||
/**
|
/**
|
||||||
* 正文**纯文本**(把 `note_blocks` 里 text 块拼起来,图片块记成「[图片]」)。
|
* 正文**纯文本**(只把 `note_blocks` 里的 text 块拼起来,图片块不进正文)。
|
||||||
*
|
*
|
||||||
* 🔴 v65(用户 2026-10-20「笔记现在也点不进去,记笔记也记不了」):
|
* 🔴 v65(用户 2026-10-20「笔记现在也点不进去,记笔记也记不了」):
|
||||||
* 服务端 `/notes` 列表其实**已经带 `blocks`**(`Note::attachChildren` 一次性挂上),
|
* 服务端 `/notes` 列表其实**已经带 `blocks`**(`Note::attachChildren` 一次性挂上),
|
||||||
* 只是端上原来只用它算了个摘要扔掉 ⇒ 点进详情时正文没了。
|
* 只是端上原来只用它算了个摘要扔掉 ⇒ 点进详情时正文没了。
|
||||||
* 这里存下来,详情直接显示,不必再打一次 `/notes/<id>`。
|
* 这里存下来,详情直接显示,不必再打一次 `/notes/<id>`。
|
||||||
|
* 🔴 v95+(工单 #23):图片块另存 `blocks` 字段,不再挤进正文当「[图片]」。
|
||||||
*/
|
*/
|
||||||
body: string;
|
body: string;
|
||||||
|
/**
|
||||||
|
* 正文的**块**(服务端 `note_blocks` 的镜像,工单 #23)。
|
||||||
|
*
|
||||||
|
* 🔴 为什么要在 `body`(纯文本摘要)之外再存一份块:
|
||||||
|
* 端上要"图文混排"就不能只拿一条拼好的文本 —— 图片块的内容是
|
||||||
|
* 附件 key,`body` 里被记成了「[图片]」;要真显示图、以及编辑时
|
||||||
|
* **不把图片块清掉**,都得拿到原始块(`kind` + `content` + `position`)。
|
||||||
|
* 服务端列表(`Note::all` + `attachChildren`)本就下发了 `blocks`,
|
||||||
|
* 这里不再用一次就扔。
|
||||||
|
*/
|
||||||
|
blocks: FbNoteBlock[];
|
||||||
/** 是不是我写的(不是的话只能看,服务端也只让作者删) */
|
/** 是不是我写的(不是的话只能看,服务端也只让作者删) */
|
||||||
mine: boolean;
|
mine: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一条笔记正文块(kind ∈ text / image) */
|
||||||
|
export interface FbNoteBlock {
|
||||||
|
/** text / image */
|
||||||
|
kind: string;
|
||||||
|
/** text = 文本;image = 附件 key(可能带 `|宽度` 后缀,取图时先剥) */
|
||||||
|
content: string;
|
||||||
|
/** 块顺序 */
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 通知(notifications)
|
// 通知(notifications)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { FbColors, FbMetric, FbTint, fbAlpha } from '../../common/Theme';
|
import { FbColors, FbMetric, FbTint, fbAlpha } from '../../common/Theme';
|
||||||
import { FbAppState, fbAppState } from '../../common/Store';
|
import { FbAppState, fbAppState } from '../../common/Store';
|
||||||
import { FbNoteItem, FbNotificationItem } from '../../model/Types';
|
import { FbNoteItem, FbNoteBlock, FbNotificationItem } from '../../model/Types';
|
||||||
import { parseNotes, parseNotifications } from '../../data/FbParse';
|
import { parseNotes, parseNotifications } from '../../data/FbParse';
|
||||||
import { loadModule } from '../../data/FbModuleLoad';
|
import { loadModule } from '../../data/FbModuleLoad';
|
||||||
import { FbAvatar, FbDot, FbEmptyBox, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
import { FbAvatar, FbDot, FbEmptyBox, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||||
@@ -53,6 +53,8 @@ export struct FbNotesPage {
|
|||||||
@Local sheetId: number = 0;
|
@Local sheetId: number = 0;
|
||||||
@Local sheetTitle: string = '';
|
@Local sheetTitle: string = '';
|
||||||
@Local sheetBody: string = '';
|
@Local sheetBody: string = '';
|
||||||
|
/** 正文块(工单 #23:图文混排要原始块) */
|
||||||
|
@Local sheetBlocks: FbNoteBlock[] = [];
|
||||||
@Local sheetTags: string = '';
|
@Local sheetTags: string = '';
|
||||||
@Local sheetMine: boolean = true;
|
@Local sheetMine: boolean = true;
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ export struct FbNotesPage {
|
|||||||
this.sheetId = 0;
|
this.sheetId = 0;
|
||||||
this.sheetTitle = '';
|
this.sheetTitle = '';
|
||||||
this.sheetBody = '';
|
this.sheetBody = '';
|
||||||
|
this.sheetBlocks = [];
|
||||||
this.sheetTags = '';
|
this.sheetTags = '';
|
||||||
this.sheetMine = true;
|
this.sheetMine = true;
|
||||||
this.sheetOpen = true;
|
this.sheetOpen = true;
|
||||||
@@ -97,6 +100,7 @@ export struct FbNotesPage {
|
|||||||
this.sheetId = n.id;
|
this.sheetId = n.id;
|
||||||
this.sheetTitle = n.title;
|
this.sheetTitle = n.title;
|
||||||
this.sheetBody = n.body;
|
this.sheetBody = n.body;
|
||||||
|
this.sheetBlocks = n.blocks;
|
||||||
this.sheetTags = n.tags.join(' ');
|
this.sheetTags = n.tags.join(' ');
|
||||||
this.sheetMine = n.mine;
|
this.sheetMine = n.mine;
|
||||||
this.sheetOpen = true;
|
this.sheetOpen = true;
|
||||||
@@ -194,6 +198,7 @@ export struct FbNotesPage {
|
|||||||
editingId: this.sheetId,
|
editingId: this.sheetId,
|
||||||
editingTitle: this.sheetTitle,
|
editingTitle: this.sheetTitle,
|
||||||
editingBody: this.sheetBody,
|
editingBody: this.sheetBody,
|
||||||
|
editingBlocks: this.sheetBlocks,
|
||||||
editingTags: this.sheetTags,
|
editingTags: this.sheetTags,
|
||||||
mine: this.sheetMine,
|
mine: this.sheetMine,
|
||||||
onClose: (): void => {
|
onClose: (): void => {
|
||||||
|
|||||||
Reference in New Issue
Block a user