ReviewReply、REV-146、表单内容及日志均为工程设计示例,非华为应用市场的真实审核工单,也不是 DevEco 或真机测试结果。本文的本地校验规则不代表平台审核标准。

一、先从一个反常的输入体验说起

上架材料提交前,运营或开发人员经常需要编辑一段说明:补充权限用途、回应截图问题,或者解释某处页面交互。文本并不长,却很容易发生一种令人困惑的现象——用户把光标移动到段落中间修改一个词,下一次输入时光标突然跳到末尾;与此同时,页面还可能显示上一版文字的“校验已通过”,仿佛当前新稿也通过了检查。

这两个症状看起来都发生在一个文本框里,本质却是两条独立的状态链。光标漂移通常要检查文本值何时被重新赋给组件;旧提示覆盖则要检查异步校验的结果是否仍属于当前草稿。把它们混成一个“输入框偶发异常”,往往只能得到不断 setTimeout 的临时补丁。

因此设计一个 ReviewReply 页面 ReviewPage,用 REV-146 作为本地草稿编号。当前演示文字是“已补充权限用途说明,截图也已更新。”,共 17 个字符,草稿版本 v7;页面展示 17/500 和“本地检查通过”。500 是 Demo 自己的产品上限,不是 HarmonyOS 或应用市场的统一字数政策。图中使用的 LOCAL_OK 仅代表本地两个规则返回空问题集合,不代表资料已经提交、更不代表平台审核通过。

二、先审视绑定,再决定怎样校验

1. 不要在每次输入时强行格式化整段内容

ArkUI TextArea 支持将文本值传给组件,也支持通过 onChange 接收用户修改。如果开发者在 onChange 之后马上给 text 赋一个经过格式化的新字符串,光标就可能因为内容重新设置而被移到末尾。华为 2026 年 6 月发布的 TextArea 常见问题说明也专门提醒:应该把状态变量与文本值实时绑定,避免刷新后文本内容异常。这里不做自动空格插入、不在编辑过程中改写标点,只维护原文。

建议将页面放在 pages/ReviewPage.ets,将规则归入 model/DraftRules.ets,将版本栅栏封装到 utils/DraftVersion.ets,对应后文的独立职责。第一段代码解决的是“同一份草稿到底由谁持有”。业务状态保存在 @State draft,输入框通过 text 读取,并在 onChange 原样写回。页面上的字符计数直接从同一变量计算。为保持示例容易理解,演示不主动控制光标坐标,不在每个字符变化时重建整个 ReviewPage。

@Entry
@Component
struct ReviewPage {
  @State draft: string = '已补充权限用途说明,截图也已更新。';
  @State revision: number = 7;
  @State checkState: string = 'LOCAL_OK';
  private checkTimer: number = -1;
  private active: boolean = true;

  build() {
    Column({ space: 12 }) {
      Text('上架反馈草稿').fontSize(24)
      Text('草稿 REV-146')
      TextArea({ text: this.draft, placeholder: '说明本次修改' })
        .maxLength(500)
        .height(180)
        .onChange((value: string) => { this.onEdit(value); })
      Text(`${this.draft.length}/500  ·  v${this.revision}`)
      Text(this.checkState === 'LOCAL_OK' ? '本地检查通过' : '等待本地检查')
    }.padding(20).width('100%')
  }

  onEdit(value: string): void {
    this.draft = value;
    this.revision += 1;
    this.checkState = 'CHECKING';
    this.scheduleCheck();
  }
}

这段示例没有依赖不确定的 SDK 新特性,只使用基本的 ArkUI 组件、状态和 onChange。需要注意,TextArea({ text: this.draft }) 与用户输入必须维持单向对应:改值时不要暗中插入模板前缀或者移除换行,否则光标稳定问题仍会回来。正式工程如果确实必须自动格式化,应先独立设计选择区与光标位置恢复,而不是把格式化代码塞进当前演示。

2. 把校验定义为业务函数,而不是平台审核

页面的两条本地规则很朴素:内容不能为空,长度不超过 500。它们是为了说明“输入有效性”与“异步结果归属”两个关注点。实际 AppGallery 提交还涉及隐私声明、资源、包体、资质等不同检查;不能因为一个本地字符串函数返回空数组,就标注“审核通过”。示例也不试图伪造网络校验服务。

下段纯 ArkTS 函数只接收字符串并返回问题列表,不读取 UI,也不持有定时器。把业务规则与页面隔开以后,开发者可以用同一份输入样本做同步单元测试,同时让页面只负责展示当前版本的结果。这里长度按 JavaScript 字符串的 length 计算,复杂表情字符的用户感知长度可能不同,正式产品需要另行选择字符统计策略。

function checkLocalDraft(text: string): string[] {
  const issues: string[] = [];
  if (text.trim().length === 0) {
    issues.push('请补充反馈说明');
  }
  if (text.length > 500) {
    issues.push('说明文字超过本项目上限');
  }
  return issues;
}

function toCheckState(issues: string[]): string {
  return issues.length === 0 ? 'LOCAL_OK' : 'LOCAL_BLOCKED';
}

maxLength(500) 能在界面输入层帮助限制字符数,但它不是全部校验。数据可能由草稿恢复、外部接口或测试数据注入,仍要经过 checkLocalDraft。本地约束的错误提示应该落在输入框附近,并保留用户已经输入的内容,不能因为不通过就把草稿清空。对于需要多人共同编辑的真实反馈表单,还得补充冲突合并和版本持久化策略,不能只依赖一个内存变量。

三、晚回来的结果,必须先证明自己属于当前版本

1. v6 的结论不能覆盖 v7

假设用户先提交 v6 到校验队列,又继续输入形成 v7,那么 v6 先后返回都不应修改 v7 的“校验状态”。Demo 用一个 300 毫秒定时器模拟异步工作:每次修改先取消待触发的定时器,再记录当前 revision,到时将计算结果连同版本号交回页面。即使未来把本地函数换成真正的异步校验接口,版本比较的原则也保持不变。

第三段代码解决“失效任务和页面销毁”两个边界。这里的 receiveCheck 可以被测试代码主动喂入旧版本以验证 v6 被拒绝,图中的 ignore rev=6 就是这种预期模拟日志,并非后台服务真实回包。aboutToDisappear 会清理定时器并阻止已退出页面继续接收结果。

// 以下方法放在上文 ReviewPage 组件内部。
private scheduleCheck(): void {
  if (this.checkTimer >= 0) { clearTimeout(this.checkTimer); }
  const targetRevision: number = this.revision;
  this.checkTimer = setTimeout(() => {
    const issues: string[] = checkLocalDraft(this.draft);
    this.receiveCheck(targetRevision, toCheckState(issues));
  }, 300);
}

private receiveCheck(rev: number, state: string): void {
  if (!this.active || rev !== this.revision) {
    console.info(`ReviewReply REV-146 ignore rev=${rev}`);
    return;
  }
  this.checkState = state;
  console.info(`ReviewReply REV-146 validate rev=${rev} ${state}`);
}

aboutToAppear(): void { this.active = true; }
aboutToDisappear(): void {
  this.active = false;
  if (this.checkTimer >= 0) {
    clearTimeout(this.checkTimer);
    this.checkTimer = -1;
  }
}

这里的防护分成两层:clearTimeout 处理尚未启动的工作,版本栅栏处理已经发出、无法撤回的结果。真实网络请求即使取消,也可能在某些边界时序进入回调,所以通常仍应保留“回复版本等于当前版本”的断言。尤其是在输入框短时间发生多次修改时,单纯依赖接口完成顺序无法保证 UI 显示的是最新内容。

2. 光标问题和回包问题使用两套观察点

调试光标漂移时,应该记录触发输入变更前后的文本长度、组件是否被重新创建、是否存在对 draft 的格式化赋值,并使用 DevEco Testing 的界面检查工具观察输入组件。不要只看定时器日志:即使异步任务全部取消,重新赋值仍会导致光标位置发生变化。反过来,光标正常也不能证明校验状态正确,因为 v6 仍可能覆盖 v7。

针对版本问题,可以构造三组确定性输入:v6 回调晚于 v7,页面销毁后回调到达,以及空白输入触发 LOCAL_BLOCKED。期望是旧版本被忽略、销毁后的页面不变、空白文本阻止本地通过。模拟日志采用 ReviewReply REV-146 ignore rev=6 与 ReviewReply REV-146 validate rev=7 LOCAL_OK;它们表达的只是模型预期,不应该出现在真实测试报告的“通过截图”栏目里。

四、从业务结果倒推,页面到底该承诺什么

这个页面最容易过度承诺的有两件事。其一,把 LOCAL_OK 改成“审核通过”;其二,在用户还在编辑时显示前一个版本的结论。前者是术语越界,后者是状态错配。更合理的文案是“本地检查通过”,并在真正发起提交操作时重新检查一遍当前文本与版本。即使用户没有继续敲字,也不能拿几秒前缓存的检查结果当作服务器接收成功的证据。

从交互上看,自动检查不应打断输入。错误提示可以延后出现,但不应该移动光标、不该触发整页跳转,也不应在新稿已经修改后保留旧稿的绿标。300 毫秒只是 Demo 的设计参数,不是任何官方性能建议;实际产品需要根据输入成本、校验耗时和弱网条件衡量。若检查涉及敏感的应用描述,应该说明数据究竟会不会发到服务端,避免把本地示意误写成“云端自动审核”。

生命周期也是必须交代的部分。ReviewPage 消失时清理定时器只是最低限度;如果后续接入网络请求、数据库或页面间共享状态,还需要分别处理请求取消、草稿落盘、跨窗口同步和重复提交。释放定时器能减少多余回调,却不会自动保存文字。要实现用户离开再回来仍能继续编辑,应设计独立的草稿存储层,并根据任务编号恢复内容,而不是指望组件内存一直存活。

五、适用边界和验收清单

当前方案适合文本说明编辑器、反馈单、应用上架前自检备注一类场景,核心目标是两点:输入过程中不无故改写文本,异步返回不覆盖较新的草稿。它没有实现真实应用市场审核,也没有实现真正的服务端提交。REV-146 和“检查通过”都是本地设计数据,不能拿去代替政策符合性审查。

验收时可以将结果拆成“预期行为”和“设备验证”两栏:前者可基于纯函数与版本序列进行静态检查,后者要在目标 SDK 上用输入法、复制粘贴、删除中间字符、切换页面与不同屏幕方向逐项确认。在未完成实机与 DevEco 编译前,不应写“光标问题已彻底解决”。这个边界对技术文章同样重要——可复用的代码和已证实的产品效果不是一回事。

本案例保留一个工程上的选择:宁可少做输入过程中的自动修饰,也不要让编辑体验为后台校验买单。当输入状态、校验状态和提交状态分开管理,后续无论替换存储方式还是增加网络接口,问题都能回到各自明确的责任范围,而不是反复修补一个越来越复杂的 onChange 回调。

六、官方资料与未验证事项

本文参考华为官方《任意位置删除字符时光标总会跳转到最后删除字符》以及 TextArea 输入组件相关指南,依据其对 TextArea、onChange、文本值回写和光标问题的说明选择“原样双向同步,不做每次输入重格式化”的路线。maxLength(500)、REV-146 与版本校验器属于本案例的业务设计,不是 AppGallery 政策。正式环境仍需按实际 SDK/API 页面确认所用组件属性并进行编译、输入法与设备验证。

  • 华为 ArkUI TextArea 常见问题(2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1615
  • 华为《自定义键盘》(2026-09-14,供理解输入组件与键盘生命周期):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-customize-keyboard
Logo

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

更多推荐