ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 EditableTitleBarV2:保存成功后还有未保存修改,异步回调该认哪一版?

HarmonyOS 7 EditableTitleBarV2:保存成功后还有未保存修改,异步回调该认哪一版? HarmonyOS 7 EditableTitleBarV2保存成功后还有未保存修改异步回调该认哪一版在编辑页输入 A点右上角的保存网络还没返回又把内容改成 B。几秒后页面显示“已保存”但服务端实际收到的只有 A。这时退出页面刚刚输入的 B 就可能丢失。换成 HarmonyOS 7 的 EditableTitleBarV2也不会自动解决这个问题。标题栏负责展示操作入口哪一版内容已保存、哪一笔请求仍在执行要由编辑页自己维护。本文做一个不接真实业务的便签编辑页允许保存期间继续输入但同一时刻只发一笔保存请求。重点不是多加一个 loading而是让回调认得自己提交的快照。适用范围EditableTitleBarV2 起始版本为 26.0.0仅用于 Stage 模型本文按 HarmonyOS 7 / API 26 文档讨论。官方页面更新于 2026-09-09 14:54核对日期为 2026-09-27。文中的状态模型和两组断言已在本机 Node.js 运行ArkTS 页面未在 API 26 SDK 或真机编译运行。模拟存储不会写入文件或服务器不能把它当成持久化实现。先纠正一个容易写错的按钮配置看起来都是标题栏右侧的按钮配置却不是同一套。官方配置实际用途不能混用的地方EditableSaveButtonV2.onAction内置保存按钮的点击回调不是菜单项的 actionEditableSaveButtonV2.isRequired是否显示内置保存按钮默认 true不是是否启用false 会隐藏按钮EditableTitleBarMenuItemV2.isEnabled自定义菜单项是否可操作不能照搬到 EditableSaveButtonV2 上EditableTitleV2.mainTitle / subTitle可观察的主副标题改标题不会替业务完成保存EditableTitleBarStyleV2背景、安全区域等标题栏样式不是保存状态容器截至这次核对内置保存按钮的配置表只有 isRequired、defaultFocus、onAction没有 isEnabled。给标题栏链一个通用 enabled 属性也不稳妥官方说明通用属性可能挂在额外生成的Common节点上并不直接作用于标题栏本身不建议这样设置。因此下面保留内置保存入口在回调入口拦住重复提交并在页面上显示状态。它并没有把图标变灰。假如产品要求真正的禁用样式可以隐藏内置保存按钮换成支持 isEnabled 的自定义菜单项但菜单项的回调仍然需要业务保护不能只靠视觉状态。把三个问题分开才不会修好一个又弄坏另一个一次保存至少涉及三份信息当前编辑版本 revision内容每发生一次实际变化就递增。当前请求 active这笔请求带走的文本、版本和请求编号。最后确认的版本 acknowledgedRevision收到成功响应的那一版而不是响应回来时屏幕上的版本。saving 由“有没有当前请求”推导dirty 由“当前版本与已确认版本是否相同”推导。两者不是反义词请求已经结束仍然可以有未保存修改。一种常见错误是回调发现版本过期直接 return。虽然没有误把 B 标成已保存但如果 return 前没释放这笔请求页面会一直处于保存中。正确顺序是先判断这个回调是否属于当前请求再结束该请求最后确认它提交的版本。不能用编辑版本代替请求身份。可复用的状态模型将下面代码作为 SaveModel.ets 使用它不依赖 ArkUI。为方便在电脑上验证同一段代码也可以保存为 SaveModel.ts由支持类型擦除的 Node.js 运行。这里没有把编辑版本号冒充服务端的并发版本号。export class SaveTicket { readonly id: number; readonly revision: number; readonly text: string; constructor(id: number, revision: number, text: string) { this.id id; this.revision revision; this.text text; } } export class SaveModel { text: string ; revision: number 0; acknowledgedRevision: number 0; private nextId: number 0; private active: SaveTicket | undefined undefined; private disposed: boolean false; get saving(): boolean { return this.active ! undefined; } get dirty(): boolean { return this.revision ! this.acknowledgedRevision; } edit(text: string): void { if (this.disposed || text this.text) return; this.text text; this.revision; } begin(): SaveTicket | undefined { if (this.disposed || this.saving || !this.dirty) return undefined; const ticket new SaveTicket(this.nextId, this.revision, this.text); this.active ticket; return ticket; } succeed(ticket: SaveTicket): boolean { if (this.disposed || this.active ! ticket) return false; this.active undefined; this.acknowledgedRevision ticket.revision; return true; } fail(ticket: SaveTicket): boolean { if (this.disposed || this.active ! ticket) return false; this.active undefined; return true; } dispose(): void { this.disposed true; this.active undefined; } }active 持有本次创建的对象回调必须携带同一个 ticket旧回调便不能结束后来的请求。调用方不要自行修改模型字段或拼造 ticket如果接入跨线程或跨进程消息应改成校验稳定的请求 ID 和编辑会话 ID不能继续依赖对象引用相等。这个模型采用保守的 dirty 判定输入 A 后改成 B再改回 A仍视为发生过新编辑。这样不会漏保存代价是可能多保存一次。要按内容判断是否相同可以另外记录已确认内容的摘要但不要取消请求身份校验。案例一保存 A 的时候继续把内容改成 B复现顺序是固定的不需要依赖网速碰运气edit(A) → begin() → edit(B) → succeed(ticket)。用同一份模型跑下面断言。以下两个测试代码块都接在模型代码后执行不需要再复制一份模型实现。function check(value: boolean, message: string): void { if (!value) throw new Error(message); } const first new SaveModel(); first.edit(A); const requestA first.begin(); if (requestA undefined) throw new Error(A should start); check(first.begin() undefined, double tap must not submit twice); first.edit(B); check(requestA.text A, submitted snapshot must remain A); check(first.succeed(requestA), current request should finish); check(!first.saving, A finished: saving must end); check(first.dirty, B has not been acknowledged); check(first.text B, callback must not overwrite the editor); const requestB first.begin(); if (requestB undefined) throw new Error(B should start); check(first.succeed(requestB), B should finish); check(!first.dirty !first.saving, latest revision is now saved); check(first.begin() undefined, unchanged text needs no request); console.log(case 1 passed);第一笔成功后页面应该显示“仍有未保存修改”而不是继续转圈也不是“全部已保存”。第二次保存发送 B成功后才消除 dirty。这就是区分两种状态的实际收益允许继续输入又不替未提交的文字承诺保存成功。案例二失败后重试旧回调不能结束新请求普通 Promise 只会兑现一次但接入重试层、事件回调或超时包装时同一个操作可能收到多条通知。测试里刻意再送一次旧通知用来验证边界不代表 Promise 会先失败再成功。const second new SaveModel(); second.edit(待保存内容); const failed second.begin(); if (failed undefined) throw new Error(first attempt should start); check(second.fail(failed), failure must release active request); check(second.dirty !second.saving, failed content remains editable); const retry second.begin(); if (retry undefined) throw new Error(retry should start); check(retry.id ! failed.id, retry needs a new request identity); check(!second.succeed(failed), late old success must be ignored); check(!second.fail(failed), late old failure must be ignored); check(second.saving, old notification must not release the retry); check(second.succeed(retry), retry succeeds normally); check(!second.dirty, retry acknowledged the submitted revision); second.edit(离开页面前的新内容); const leaving second.begin(); if (leaving undefined) throw new Error(last request should start); second.dispose(); check(!second.succeed(leaving), closed editor ignores its callback); console.log(case 2 passed);本机执行结果为 case 1 passed、case 2 passed。它证明这份状态模型覆盖了双击、继续编辑、失败释放、旧通知和销毁后的回调不证明网络接口已落库也不证明 ArkUI 已正确刷新。接入 EditableTitleBarV2界面刷新也要有明确来源下面是页面接入示例SaveModel.ets 与页面放在同一目录。模型保持普通类界面用 Local 字段接收投影结果每次编辑、开始保存或收到结果都调用 refresh。这样不用误以为一个普通类内部字段变化会自动更新全部界面。import { EditableTitleBarV2, EditableSaveButtonV2, EditableTitleBarStyleV2 } from kit.ArkUI; import { SaveModel, SaveTicket } from ./SaveModel; Entry ComponentV2 struct SaveSnapshotPage { private model: SaveModel new SaveModel(); private alive: boolean true; Local text: string ; Local stateText: string 没有未保存修改; Local failNext: boolean false; private refresh(): void { this.stateText this.model.saving ? 保存中可以继续输入 : (this.model.dirty ? 仍有未保存修改 : 当前内容已确认); } private mockStore(ticket: SaveTicket, fail: boolean): Promisevoid { // Only simulates delay and failure; does not persist ticket.text. return new Promisevoid((resolve, reject) { setTimeout(() { if (fail) reject(new Error(模拟保存失败)); else resolve(); }, 3000); }); } private save(): void { const ticket this.model.begin(); if (ticket undefined) return; const shouldFail this.failNext; this.failNext false; this.refresh(); this.mockStore(ticket, shouldFail).then(() { if (this.alive this.model.succeed(ticket)) this.refresh(); }).catch(() { if (this.alive this.model.fail(ticket)) { this.refresh(); this.stateText 保存失败内容保留可再次保存; } }); } aboutToDisappear(): void { this.alive false; this.model.dispose(); } build() { Column({ space: 16 }) { EditableTitleBarV2({ title: 便签编辑, saveButton: new EditableSaveButtonV2({ onAction: () { this.save(); } }), options: new EditableTitleBarStyleV2() }) TextArea({ text: this.text, placeholder: 输入 A 后保存再改为 B }) .height(200) .onChange((value: string) { this.text value; this.model.edit(value); this.refresh(); }) Text(this.stateText) Toggle({ type: ToggleType.Switch, isOn: this.failNext }) .onChange((value: boolean) { this.failNext value; }) Text(开启开关后下一次模拟保存失败) }.width(100%) } }页面示例把 aboutToDisappear 视为编辑实例结束销毁后不复用这个模型。如果采用缓存页面、导航隐藏后再次显示的结构要先明确“隐藏”和“会话结束”的区别再把 dispose 放到真正的结束位置不能照搬到所有生命周期回调。在支持 API 26 的环境里应至少人工核对以下结果输入 A 点保存三秒内改成 B返回后显示仍有修改再点一次返回后显示当前内容已确认。开启失败开关后保存失败提示出现文本不丢失重新点击能开始新请求。连续点击保存时存储层只应被调用一次。这里是待设备端执行的验收步骤不是已完成的真机结果。三种方案怎么选方案好处代价与适用边界保存期间锁住编辑最容易保证界面内容与提交快照一致慢网络下阻断输入长文本体验差允许编辑串行保存快照不阻断输入请求归属简单完成后可能仍需再次保存本文采用它每次输入都自动保存操作省心要处理节流、失败重试、写入顺序和服务端冲突不能只加 debounce对一个有明确保存按钮的编辑页我优先选第二种。这个选择并不是性能优化结论而是状态复杂度与输入体验之间的取舍。客户端串行化只能约束当前编辑实例多设备同时修改同一条记录仍可能互相覆盖。接入真实后端时应使用服务端版本条件或等价的冲突检测失败重试也要有幂等约定。请求超时只表示客户端没拿到结果不等于服务器没有写入不能据此承诺重试绝不会重复写。真正可以复用的是 SaveModel 这层而不是整个便签页面。替换 mockStore 为存储适配器时传 ticket.text不要在请求真正执行时重新读取 this.text只有持久化语义明确成功才调用 succeed。上传附件、后台同步、草稿自动保存也能借用“请求身份快照版本”的思路但各自的事务边界必须另行定义。下次遇到“保存状态不对”按这个顺序查先看按钮配置有没有用错类再看请求带走的是快照还是可变对象。随后检查回调是否只处理自己的请求、失败是否释放 active、成功是否只确认提交版本、页面结束后是否还在写 UI。最后才考虑加自动保存或更复杂的缓存。这个例子解决的不是“让标题栏多显示一个勾”而是让这个勾不再对未提交的内容作出错误承诺。若你遇到过保存后文字回退可以对照日志中的请求编号、提交版本、当前版本先判断是旧回调污染还是服务端并发覆盖两者的修法不一样。官方参考EditableTitleBarV2组件限制与设备范围EditableSaveButtonV2Options显示、焦点与回调字段EditableTitleBarMenuItemV2自定义菜单项配置文中的请求状态模型是应用侧设计不是平台内置保存机制示意图也不是设备截图。
返回列表