笔记图片块:展示真图 + 插图 + 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:「笔记现在也点不进去,记笔记也记不了。」
|
||||
* ⇒ 端上既没有新建、也没有详情:`FbPrimaryButton` 无 `onTap`、卡片无 `onClick`。
|
||||
*
|
||||
* ⚠️ 端上只支持**纯文本**(服务端 `body` → 一个 `text` 块):
|
||||
* 网页那边可以图文混排(`note_blocks` 有 image 块),端上先不做 ——
|
||||
* 图片块的内容是附件 key,硬拼进文本框会变成一串乱码。
|
||||
* 所以已有笔记里的图片块在端上显示成「[图片]」(见 `parseNotes` 的 `noteBodyOf`)。
|
||||
* 🔴 工单 #23(用户 2026-10-28):图文混排。原来端上只支持纯文本——
|
||||
* · 图片块显示成「[图片]」占位;
|
||||
* · 更糟:保存时只传 `body`,服务端折成单个 text 块**整组替换**,
|
||||
* 编辑一条带图笔记会把图片块**静默删光**。
|
||||
* 现在:
|
||||
* · `editingBlocks` 把原始块(text/image)带进来,image 块真画图;
|
||||
* · 「插图」按钮 → 选图 → 上传拿 key → 追加到图片列表;
|
||||
* · 保存走 `saveNoteWithBlocks`(正文 text 块 + 图片 image 块整组提交)。
|
||||
*
|
||||
* ⚠️ **只读**:别人共享给我的笔记能看不能改(服务端 `Note::canWrite` 那一套)。
|
||||
* 端上按 `mine` 判断 —— 保守一点宁可少给编辑,也不能给个点了报 403 的按钮。
|
||||
@@ -20,8 +24,15 @@ 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 { 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 { picker } from '@kit.CoreFileKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbNoteSheet {
|
||||
@@ -31,6 +42,8 @@ export struct FbNoteSheet {
|
||||
@Param editingId: number = 0;
|
||||
@Param editingTitle: string = '';
|
||||
@Param editingBody: string = '';
|
||||
/** 正文原始块(工单 #23:image 块借此真画图;为空则回到纯文本) */
|
||||
@Param editingBlocks: FbNoteBlock[] = [];
|
||||
@Param editingTags: string = '';
|
||||
/** 是不是我写的(不是就只能看) */
|
||||
@Param mine: boolean = true;
|
||||
@@ -41,6 +54,10 @@ export struct FbNoteSheet {
|
||||
|
||||
@Local title: string = '';
|
||||
@Local body: string = '';
|
||||
/** 图片块 key(工单 #23:已存在的 + 新插的,按序保存) */
|
||||
@Local imageKeys: string[] = [];
|
||||
/** 正在上传插图 */
|
||||
@Local uploadingImg: boolean = false;
|
||||
/** 标签输入(用空格或逗号分隔,服务端会自己归一) */
|
||||
@Local tags: string = '';
|
||||
@Local saving: boolean = false;
|
||||
@@ -51,6 +68,17 @@ export struct FbNoteSheet {
|
||||
this.title = this.editingTitle;
|
||||
this.body = this.editingBody;
|
||||
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%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 图片块在端上只能显示成「[图片]」—— 说清楚,别让人以为内容丢了
|
||||
if (this.editingId > 0 && this.body.indexOf('[图片]') >= 0) {
|
||||
Text('这条笔记里的图片在网页端看;App 这边先显示成「[图片]」。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.maxLines(2)
|
||||
// ---- 图片(工单 #23:真画图,不显示「[图片]」占位)----
|
||||
if (this.imageKeys.length > 0) {
|
||||
Column({ space: 8 }) {
|
||||
ForEach(this.imageKeys, (key: string, idx: number) => {
|
||||
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%')
|
||||
.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%')
|
||||
@@ -287,19 +354,23 @@ export struct FbNoteSheet {
|
||||
}
|
||||
const t: string = this.title.trim();
|
||||
const b: string = this.body.trim();
|
||||
if (t === '' && b === '') {
|
||||
this.note = '标题和正文至少写一样(服务端不允许存一条空笔记)。';
|
||||
const hasImg: boolean = this.imageKeys.length > 0;
|
||||
if (t === '' && b === '' && !hasImg) {
|
||||
this.note = '标题、正文或图片至少填一样(服务端不允许存一条空笔记)。';
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
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 res: FbApiResult = this.editingId > 0
|
||||
? await updateNote(token, this.editingId, useTitle, b, this.tagList())
|
||||
: await createNote(token, useTitle, b, this.tagList());
|
||||
// 🔴 工单 #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 createNote(token, useTitle, b, this.tagList()));
|
||||
this.saving = false;
|
||||
if (!res.ok) {
|
||||
this.note = res.error !== '' ? res.error : '保存失败,请稍后再试。';
|
||||
@@ -308,6 +379,53 @@ export struct FbNoteSheet {
|
||||
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> {
|
||||
if (this.saving) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user