本文使用 FoldProof 演示工程,页面 ReviewWorkbench,任务 FP-2010。文中界面、HiLog 行和接口回执是为了讲清状态流转而构造的演示数据,并非真机实测。涉及页面布局时,只讨论 Tabs 切页;折叠屏的真实展开态感知和窗口尺寸适配需要另行接入对应能力。

一、编辑到一半切页,真正丢掉的未必是文本

在一个图像质检工作台里,审核员对素材 R-037 输入了“镜像边缘有重影”,随后切到“规则说明”核对验收口径,再切回来补一句。表面看,这只是两个 TabContent 之间来回切换;实际项目里,却经常把编辑器的当前值、服务端校验结果和当前显示页签混成同一个状态。先触发校验、再改字、再切页,最容易出现一种尴尬:文字还在,提示却显示上一次文本“校验通过”。

这次我把问题缩到极小:项目名 FoldProof,页面 ReviewWorkbench,只有“待核验”和“规则说明”两个页签;数据对象只有 R-037,草稿当前版本是 v5。第一轮请求曾携带 v4,但它不能改变 v5 的展示结果。这样设计不是为了伪造一个复杂产品,而是让一次并发边界的因果关系可以在代码和截图里逐项对上。

以前我会把切页恢复、提交防抖、网络请求全塞进页面的 onChange,后来发现最好先追问三个问题:谁拥有草稿?谁拥有校验结论?谁有资格写 UI?Tabs 只负责展示哪个内容区,不应该成为草稿版本的来源,更不应该负责网络回执的合法性。

二、先把三个状态分开,避免“看起来同步”

需要保护的是三条互不等价的事实。currentTab 是视图位置,只取 0 或 1;editorText 是用户当前编辑的文本;draftVersion 则表示该文本经历过多少次有效修改。版本不是服务器时间戳,也不是图像素材的全局版本号,只是当前页面会话里用来判定回执新旧的单调序号。

requestSeq 进一步解决同一草稿版本被连续提交的问题:哪怕两个请求都标着 v5,后发的那一次也应该拥有最终 UI 的展示权。它与 draftVersion 配合,才构成足够明确的回写条件。切换到规则页可以保留草稿,用户再次输入必须让旧校验失效。退出页面还需有 disposed 标志,不能指望 TabContent 是否可见来猜测异步回调是否安全。

先写一个与 UI 无关的回执判定函数,便于独立检查边界。这里的 replyVersion 和 replySeq 来自调用发起时保存的快照,不能收到回执后临时从页面字段补取,否则旧结果会被包装成新版本。

interface VerifyReply {
  itemId: string;
  version: number;
  seq: number;
  accepted: boolean;
}

function canApplyReply(reply: VerifyReply, itemId: string,
  draftVersion: number, requestSeq: number, disposed: boolean): boolean {
  return !disposed && reply.itemId === itemId &&
    reply.version === draftVersion && reply.seq === requestSeq;
}

这样做的好处是,规则不依赖 ArkUI 的重建时机。页面存活、条目一致、草稿版本一致、请求轮次一致,四项都满足才准许覆盖提示。v4 回执即使成功,也只是一份过期信息;显示层不需要再为它解释任何“成功”。实际产品还应让服务端具备自己的资源版本或幂等标识,本例的客户端序号不能替代服务器并发控制。

三、切页不触发提交,输入才递增草稿版本

我给 ReviewWorkbench 留下一个刻意朴素的状态层。@State 负责驱动界面可见字段;普通私有字段负责回调门禁。这种划分不是说普通字段更安全,而是避免把内部请求轮次当作需要实时渲染的 UI 数据。下面是与工程问题直接相关的处理部分,示例使用本地模拟的异步 Promise,不发送真实请求。

@State currentTab: number = 0;
@State editorText: string = '镜像边缘有重影';
@State verifyTip: string = '草稿 v5 · 待校验';
private readonly itemId: string = 'R-037';
private draftVersion: number = 5;
private requestSeq: number = 0;
private disposed: boolean = false;

private onDraftChanged(next: string): void {
  if (next === this.editorText) { return; }
  this.editorText = next;
  this.draftVersion += 1;
  this.requestSeq += 1; // 让已发请求立即失去回写资格
  this.verifyTip = `草稿 v${this.draftVersion} · 待校验`;
}

private submitPreview(): void {
  const seq: number = ++this.requestSeq;
  const version: number = this.draftVersion;
  this.verifyTip = `草稿 v${version} · 校验中`;
  Promise.resolve<VerifyReply>({
    itemId: this.itemId, version: version, seq: seq, accepted: true
  }).then((reply: VerifyReply): void => {
    if (!canApplyReply(reply, this.itemId,
      this.draftVersion, this.requestSeq, this.disposed)) { return; }
    this.verifyTip = reply.accepted ? `草稿 v${version} · 校验通过` :
      `草稿 v${version} · 需复核`;
  }).catch((): void => {
    if (!this.disposed && seq === this.requestSeq) {
      this.verifyTip = `草稿 v${version} · 校验失败`;
    }
  });
}

这段代码把“输入导致旧结果失效”和“发请求时捕获快照”放在同一视野里。注意 Promise.resolve 只是演示异步完成顺序,不是声称调用了系统 AI 校验能力,更不是生产请求适配器。真正接入网络时,需要给提交动作配置超时、错误码映射、离线反馈与重试策略;如果页面销毁时能够取消请求,应同时调用底层取消接口,但最终仍需在回执侧校验代际。

四、Tabs 保持职责单一,不能把可见性当生命周期

布局层可以非常薄:两个 TabContent 分别容纳核验编辑区和规则说明区,onChange 只记录页签索引。在折叠屏展开形态下,实际项目可以调整页签栏位置或引入双栏容器;那属于窗口响应式布局,不是本案例试图“顺便解决”的问题。把这层边界写清楚,后面调试就不会把所有草稿问题归因于折叠屏。

build() {
  Column({ space: 16 }) {
    Text('FoldProof · FP-2010').fontSize(22).fontWeight(FontWeight.Bold)
    Text(`素材 ${this.itemId}  /  ${this.verifyTip}`).fontSize(15)
    Tabs({ index: this.currentTab }) {
      TabContent() {
        Column({ space: 12 }) {
          TextArea({ text: this.editorText })
            .onChange((value: string): void => this.onDraftChanged(value))
          Button('校验当前草稿').onClick((): void => this.submitPreview())
        }
      }.tabBar('待核验')
      TabContent() {
        Text('验收规则:描述具体位置与可复现现象')
      }.tabBar('规则说明')
    }.onChange((index: number): void => { this.currentTab = index; })
  }.padding(20).width('100%')
}

aboutToDisappear(): void {
  this.disposed = true;
  this.requestSeq += 1;
}

图中 DevEco Studio 风格画面是独立绘制的结构示意:左边工程目录、中间片段、右边模拟器布局、底部 HiLog。它不是实际打开 IDE 编译后的截图,日志也不是运行设备导出的记录。约定这一点很重要,否则读者容易把“已画出来的状态”误认成“已经在 SDK 上验证过的状态”。

五、把迟到回执的复现路径写成固定剧本

复现时先打开 R-037,初始草稿版本 v5,文字是“镜像边缘有重影”。构造一个早于当前草稿的 v4 回执,标记其 accepted=true,同时让页面处在“规则说明”页。此时拒绝写回是正确结果;不能因为服务端返回成功就把展示提示改成通过,也不能因为切走了 Tab 就清空草稿。

再返回“待核验”,确认文字仍是原值;点击“校验当前草稿”,当前 v5 对应的请求获得自己的 seq,成功后才将提示更新为“草稿 v5 · 校验通过”。如果用户在校验中又输入一个字,版本进入 v6,无论 v5 回执何时回来都不得污染界面。页面退出后追加的回调同样应被忽略。

演示事件页面状态期望结果
收到旧版回执 v4规则说明页拒绝覆盖草稿提示
返回待核验文本仍是原草稿保留 R-037 当前内容
提交 v5 回执当前 seq 匹配可以显示校验通过
输入新字形成 v6已有 v5 在途旧结果不可写回

上面的手机图是纯屏幕演示:有完整状态栏,主体显示 FP-2010、R-037、草稿 v5 与当前页签。它刻意选择“已阻断 v4”这一状态,让读者核对为什么旧回执不应改变草稿;并不代表真实的 AI 审核结果。若要做自动化验收,可以把 canApplyReply 作为独立测试对象,分别覆盖不同 itemId、不同版本、不同 seq 与 disposed=true 四个拒绝条件。

六、什么时候不能继续沿用这个最小实现

单条素材只需一份 editorText,多素材切换时就必须改成以稳定 ID 为键的草稿仓库,不能单纯依赖当前索引。若跨设备编辑,还需要服务端冲突版本与用户确认路径;这和 Tabs 的 index 是两个层次。表单草稿若必须在进程重启后恢复,才轮到持久化层,切页本身不保证进程级保存。另一个常被忽略的风险是输入法组合文本:线上应用应根据最终编辑值和产品规则处理确认,不要把每次中间态都当作已经提交的审核意见。

UI 更新方面,示例把私有变量用于版本判定,没有把它们直接绑定在组件表达式里;可见的版本文本从 verifyTip 渲染。复杂页面如果出现多入口修改草稿,可以进一步收口到统一 reducer,减少局部字段互相覆盖。aboutToDisappear 负责禁止回写而不是魔法式销毁网络请求;具体资源释放仍应由网络适配器完成。

七、这次复盘给自己留下的验收线

真正值得保留的结论不是“Tabs 能留住文字”,而是不能把页面可见性、草稿内容和异步校验版本当成一回事。通过稳定素材 ID、草稿版本、请求序号和退出标志组成的一道门禁,才让切页行为不再左右业务数据。在 FoldProof 这个受控场景里,v4 被拒绝、v5 才有资格显示通过,示意图与代码中的名称和任务编号也保持一致。

本文参考华为开发者文档中心《Tabs子组件切换时触发数据刷新》(2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1476 。接口和生命周期仍需要在项目实际使用的 SDK、设备及窗口形态上编译、调试和回归;本文未声称完成真机测试。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐