HarmonyOS 表单提交实战:字段校验、防重复提交与草稿恢复

表单提交失败,经常不是提交按钮坏了,而是链路没有设计完整:字段校验散在 UI 里,用户连续点两次产生重复请求,弱网失败后内容丢失,返回页面再进来草稿没了,服务端错误无法定位到字段。表单是应用里最容易被低估的工程场景,因为它连接了用户输入、本地状态、网络提交、错误提示和数据恢复。

请添加图片描述

本文围绕一个目标展开:在 HarmonyOS 应用中设计一套可复用的表单提交链路,让字段校验、防重、提交状态、错误映射和草稿恢复都有清晰边界。

一、表单要先区分本地错误和服务端错误

本地错误适合立即提示,比如必填、长度、格式;服务端错误来自业务规则,比如用户名已存在、优惠码失效、库存变化。

错误来源 示例 处理方式
本地校验 必填为空、手机号格式错误 提交前阻止
服务端校验 名称重复、权限不足 映射到字段或顶部提示
网络异常 超时、断网 保留草稿并允许重试
重复提交 用户连续点击按钮 提交锁或请求 id 去重

请添加图片描述

如果不区分错误来源,页面会出现“明明本地能判断,却让用户等接口返回”的低效体验。

二、资料与版本边界:本文写应用层表单链路

本文示例面向 HarmonyOS NEXT / ArkTS / ArkUI 工程,重点在表单的应用层治理:字段模型、本地校验、防重复提交、草稿状态、服务端错误映射和验收排查。真实项目需要结合自己的组件库、网络库、本地存储、账号体系和后端错误码协议。

表单链路层 本文处理内容 项目适配点
字段层 值、错误、脏状态 具体输入组件
校验层 必填、长度、格式 业务规则
提交层 提交锁、请求 id、状态 网络库和接口协议
草稿层 自动保存、恢复提示 本地存储或数据库
错误层 服务端错误映射 后端错误码

请添加图片描述

表单接入前先约定错误码和草稿策略

表单链路要让用户放心,不能只靠前端校验。后端错误码是否能映射字段、草稿是否要跨账号保存、提交成功后是否清空本地内容,这些规则要在写页面前确认,否则后面会出现“接口失败但用户不知道改哪里”的问题。

约定项 推荐规则 容易踩坑的地方
字段错误码 后端返回字段名和错误消息 只返回一个通用失败文案
草稿归属 按用户 id + 表单类型隔离 多账号共用同一份草稿
提交幂等 请求携带 requestId 用户重复点击产生两条记录
成功清理 清理草稿、错误和提交锁 成功后返回仍看到旧内容
失败恢复 保留输入和服务端错误 失败后重置整张表单

建议把表单页面拆成“字段状态、校验规则、提交动作、草稿存储、错误映射”五个文件或五个对象。页面只负责渲染和触发,不要把所有判断写进按钮点击事件。

三、字段模型:值、错误和脏状态放在一起

字段不要只保存值。错误提示和是否被用户编辑过,也要属于字段状态。

export interface FormFieldState {
  name: string;
  value: string;
  error: string;
  dirty: boolean;
}

export interface RouteApplyForm {
  title: FormFieldState;
  phone: FormFieldState;
  reason: FormFieldState;
}

export function createField(name: string, value: string): FormFieldState {
  return {
    name,
    value,
    error: '',
    dirty: false
  };
}

这段模型的边界是“描述输入状态”。它让页面能够区分初始空值和用户编辑后的错误,避免一打开页面就满屏红字。

四、本地校验:提交前先把明显问题拦住

本地校验要返回字段级错误,方便页面把提示放到具体输入项下面。

export interface ValidationResult {
  valid: boolean;
  errors: Record<string, string>;
}

export function validateRouteApplyForm(form: RouteApplyForm): ValidationResult {
  const errors: Record<string, string> = {};
  if (form.title.value.trim().length === 0) {
    errors.title = '请输入路线标题';
  }
  if (!/^1[3-9][0-9]{9}$/.test(form.phone.value.trim())) {
    errors.phone = '请输入正确的手机号';
  }
  if (form.reason.value.trim().length < 10) {
    errors.reason = '申请说明至少 10 个字';
  }
  return {
    valid: Object.keys(errors).length === 0,
    errors
  };
}

校验函数不操作 UI,也不提交接口。它只告诉页面哪些字段不合法。这样表单规则可以被单元测试覆盖,也可以被多个页面复用。

五、防重复提交:按钮置灰之外还要有请求标识

只靠按钮置灰不够。弱网或页面重建时,重复请求仍可能发生。建议为每次提交生成 requestId。

export interface SubmitTicket {
  requestId: string;
  formName: string;
  createdAt: number;
}

export class SubmitLock {
  private activeRequestId = '';

  create(formName: string): SubmitTicket | undefined {
    if (this.activeRequestId.length > 0) {
      return undefined;
    }
    const requestId = `${formName}_${Date.now()}`;
    this.activeRequestId = requestId;
    return { requestId, formName, createdAt: Date.now() };
  }

  release(requestId: string): void {
    if (this.activeRequestId === requestId) {
      this.activeRequestId = '';
    }
  }
}

这个锁的职责是控制同一表单同一时间只有一次提交。后端如果也支持 requestId 去重,前后端会更稳。

六、草稿保存:失败和离开页面都不应该丢内容

表单内容通常是用户主动输入的,丢失成本很高。可以在字段变化后保存草稿,在提交成功后删除草稿。

export interface FormDraft {
  draftId: string;
  formName: string;
  values: Record<string, string>;
  updatedAt: number;
}

export function buildFormDraft(formName: string, form: RouteApplyForm): FormDraft {
  return {
    draftId: `${formName}_draft`,
    formName,
    values: {
      title: form.title.value,
      phone: form.phone.value,
      reason: form.reason.value
    },
    updatedAt: Date.now()
  };
}

export function draftHasContent(draft: FormDraft): boolean {
  return Object.values(draft.values).some(value => value.trim().length > 0);
}

这段代码把草稿转换为普通数据对象,方便接入 Preferences、数据库或文件存储。提交成功后清理草稿,提交失败则保留。

七、服务端错误映射:能定位字段就不要只弹 Toast

服务端返回的错误如果能对应字段,就应该显示到字段下方;无法对应时再放到顶部或弹窗。

export interface ServerFormError {
  code: string;
  field: string;
  message: string;
}

export function mapServerErrorsToFields(
  form: RouteApplyForm,
  errors: ServerFormError[]
): RouteApplyForm {
  const next: RouteApplyForm = {
    title: { ...form.title },
    phone: { ...form.phone },
    reason: { ...form.reason }
  };

  for (const error of errors) {
    if (error.field === 'title') {
      next.title.error = error.message;
    } else if (error.field === 'phone') {
      next.phone.error = error.message;
    } else if (error.field === 'reason') {
      next.reason.error = error.message;
    }
  }
  return next;
}

字段映射能减少用户猜测成本。比如“手机号已被占用”应该出现在手机号输入项下面,而不是只弹一个全局错误。

八、表单提交问题排查表

表单失败表现 优先查看的状态 定位入口 修复动作
一打开页面就全是错误 没有 dirty 状态 查看 FormFieldState.dirty 只在编辑后或提交后展示错误
连点提交产生两条记录 只做按钮置灰 检查 SubmitLock 和 requestId 前端锁定,后端去重
弱网失败后内容丢失 没有草稿保存 查看 FormDraft 字段变化后保存草稿
服务端错误看不懂 错误没有映射字段 检查 ServerFormError.field 字段错误展示在输入项下
提交后仍恢复旧草稿 成功后未清理草稿 查看草稿删除流程 提交成功后删除 draftId
手机号格式误判 正则或国家地区规则不匹配 检查本地校验规则 按业务地区调整规则

排查表单问题时要按输入、校验、提交、草稿、错误映射五段看。只盯提交接口,会漏掉大部分体验问题。

九、表单上线前验收表

表单验收路径 通过条件
字段校验 必填、长度、格式都有本地提示
防重复提交 连续点击不会发出重复有效请求
草稿恢复 返回再进入能恢复未提交内容
提交成功 成功后清理提交锁和草稿
提交失败 保留输入内容并显示可理解错误
服务端错误 能映射字段的错误显示在字段下方
弱网测试 超时、断网、重试路径都可恢复

验收时至少要覆盖空表单、错误格式、弱网提交、重复点击、服务端字段错误和提交成功六条路径。

复盘重复提交时先看 requestId

重复提交不是简单把按钮置灰就能完全解决。用户可能在弱网下连点,也可能从两个入口提交同一份草稿。更稳的做法是给每次有效提交生成 requestId,页面、网络层和后端日志都围绕这个 id 复盘。

export interface SubmitAudit {
  requestId: string;
  formType: string;
  userId: string;
  submitAt: number;
  fieldCount: number;
}

export function createSubmitAudit(formType: string, userId: string, fieldCount: number): SubmitAudit {
  return {
    requestId: `${formType}_${userId}_${Date.now()}`,
    formType,
    userId,
    submitAt: Date.now(),
    fieldCount
  };
}

这段审计对象不替代表单数据,它只描述一次提交行为。排查时如果同一个用户短时间内产生多个 requestId,说明页面防重或后端幂等还需要加强;如果只有一个 requestId 但生成了多条业务记录,就要看服务端幂等处理。

表单落地时按“先保护输入,再提交”的顺序做

第一步先建立字段状态。每个字段至少有 valuedirtyerror,不要让页面只拿字符串渲染。

第二步写本地校验。必填、长度、格式这类问题必须在提交前拦住,用户不应该为了一个空字段等待接口返回。

第三步接草稿保存。用户输入超过一个字段后离开页面,再回来应该能恢复。草稿按用户和表单类型隔离,避免多账号串数据。

第四步接提交锁和 requestId。按钮置灰只是体验,真正的幂等还要靠请求标识和后端约束。

第五步接服务端错误映射。能对应字段的错误放在字段下方,不能对应字段的错误放在表单顶部。不要把所有错误都做成 Toast,一闪而过用户很难修改。

十、表单相关官方资料

  1. 华为开发者文档:TextInput
    https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-textinput
  2. 华为开发者文档:Button
    https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-button
  3. 华为开发者文档:应用数据持久化
    https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-overview
  4. 华为开发者文档:网络管理
    https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/net-connection-overview

十一、把表单提交做成可靠链路

表单不是一个按钮,而是一条从输入到提交结果的链路。字段模型保存状态,本地校验拦住明显错误,提交锁防重复请求,草稿保护用户输入,服务端错误映射帮助用户修改。

提交链路问题 推荐实现方式
字段只保存 value 够吗 不够,还要保存 error 和 dirty
提交前要做什么 本地校验并生成 requestId
弱网失败怎么办 保留草稿和输入内容
错误提示放哪里 能对应字段就放字段下方
成功后清理什么 提交锁、草稿、临时错误状态
Logo

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

更多推荐