笔记图片块:展示真图 + 插图 + 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:
2026-10-28 21:35:11 +08:00
parent 3e910f1ca0
commit 97846029c7
7 changed files with 342 additions and 59 deletions
+64 -7
View File
@@ -42,15 +42,72 @@ const TAG: string = 'FbUpload';
*/
export async function fbUploadOne(path: string, field: string, uri: 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();
if (base === '') {
return '还没有设置服务器地址';
return { ok: false, body: '', error: '还没有设置服务器地址' };
}
if (token === '') {
return '还没登录';
return { ok: false, body: '', error: '还没登录' };
}
if (uri === '') {
return '没选到文件';
return { ok: false, body: '', error: '没选到文件' };
}
let buf: ArrayBuffer;
try {
@@ -61,7 +118,7 @@ export async function fbUploadOne(path: string, field: string, uri: string,
fileIo.closeSync(file);
} catch (err) {
hilog.warn(DOMAIN, TAG, '读文件失败:%{public}s', JSON.stringify(err));
return '读不出这张图';
return { ok: false, body: '', error: '读不出这张图' };
}
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
});
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 : '';
// 服务端的错误说明是 JSON 信封里的 `error` —— 尽量把它抠出来给人话
@@ -99,11 +156,11 @@ export async function fbUploadOne(path: string, field: string, uri: string,
} catch (err) {
// 不是 JSON(如 Apache 的错误页)⇒ 保留 HTTP 码
}
return msg;
return { ok: false, body: body, error: msg };
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.warn(DOMAIN, TAG, '上传失败 %{public}s:%{public}s', path, JSON.stringify(e));
return '网络异常';
return { ok: false, body: '', error: '网络异常' };
} finally {
req.destroy();
}