HarmonyOS 表单提交实战:字段校验、防重复提交与草稿恢复
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 但生成了多条业务记录,就要看服务端幂等处理。
表单落地时按“先保护输入,再提交”的顺序做
第一步先建立字段状态。每个字段至少有 value、dirty、error,不要让页面只拿字符串渲染。
第二步写本地校验。必填、长度、格式这类问题必须在提交前拦住,用户不应该为了一个空字段等待接口返回。
第三步接草稿保存。用户输入超过一个字段后离开页面,再回来应该能恢复。草稿按用户和表单类型隔离,避免多账号串数据。
第四步接提交锁和 requestId。按钮置灰只是体验,真正的幂等还要靠请求标识和后端约束。
第五步接服务端错误映射。能对应字段的错误放在字段下方,不能对应字段的错误放在表单顶部。不要把所有错误都做成 Toast,一闪而过用户很难修改。
十、表单相关官方资料
- 华为开发者文档:TextInput
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-textinput - 华为开发者文档:Button
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-button - 华为开发者文档:应用数据持久化
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-overview - 华为开发者文档:网络管理
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/net-connection-overview
十一、把表单提交做成可靠链路
表单不是一个按钮,而是一条从输入到提交结果的链路。字段模型保存状态,本地校验拦住明显错误,提交锁防重复请求,草稿保护用户输入,服务端错误映射帮助用户修改。
| 提交链路问题 | 推荐实现方式 |
|---|---|
| 字段只保存 value 够吗 | 不够,还要保存 error 和 dirty |
| 提交前要做什么 | 本地校验并生成 requestId |
| 弱网失败怎么办 | 保留草稿和输入内容 |
| 错误提示放哪里 | 能对应字段就放字段下方 |
| 成功后清理什么 | 提交锁、草稿、临时错误状态 |
更多推荐


所有评论(0)