鸿蒙ArkUI实战:交互式表单与输入校验
任何应用都离不开表单——注册登录、意见反馈、资料编辑。本文用 ArkUI 构建一个完整的注册表单,覆盖字符计数、密码强度检测、自定义单选、协议勾选和提交校验,并深入讲解表单校验的策略选择。
一、我们要做什么
一个注册页面,具备完整的表单交互能力:
- 昵称输入 + 字符计数 — 实时显示 X/20 字,超限变红提示
- 密码输入 + 强度检测 — 输入时实时计算密码强度,三色进度条 + 文字提示
- 性别选择 — 自定义单选按钮组,选中高亮填充
- 协议勾选 + 提交校验 — 全字段校验,空字段精准报错,模拟提交 Loading
和前面三篇文章不同,表单的核心挑战不在"列表数据管理",而在字段级状态管理和校验时机选择。
二、表单状态管理方案
2.1 扁平 @State vs 嵌套对象
对于表单,有两种状态组织方式:
方案 A:扁平 @State(本文采用)
@State nickname: string = '';
@State nicknameError: string = '';
@State password: string = '';
@State passwordStrength: number = 0;
@State gender: number = 0;
@State agreed: boolean = false;
方案 B:嵌套对象 + 单 @State
class FormData {
nickname: string = '';
password: string = '';
gender: number = 0;
}
@State form: FormData = new FormData();
方案 B 更接近主流前端框架的写法,但在 ArkUI 中有个致命问题:修改 form.nickname 不会触发 UI 更新(form 的引用没变)。需要用 this.form = { ...this.form, nickname: 'xxx' } 这种展开写法——对每个字段都这样做太繁琐。
方案 A 虽然变量更多,但每个 @State 独立响应,修改 nickname 只重绘相关部分,性能更好,代码也更直观。
2.2 错误信息独立管理
每个字段的错误信息用独立的 @State:
@State nicknameError: string = ''; // 空字符串 = 无错误
@State genderError: string = '';
@State agreedError: string = '';
为什么不用一个 Map<field, error>?同样的原因——Map 的内部修改不触发 @State 更新。每个错误字段独立管理,修改后立即重绘对应的错误提示。
三、交互点1:昵称输入 + 字符计数
Row() {
TextInput({ placeholder: '请输入昵称(2-20字)', text: $$this.nickname })
.fontSize(FontSize.BODY)
.layoutWeight(1)
.height(44)
.backgroundColor(Color.White)
.borderRadius(BorderRadius.MD)
.padding({ left: Spacing.MD, right: Spacing.MD })
.border({ width: 1, color: this.nicknameError ? AppColors.ERROR : AppColors.BORDER })
.onChange(() => {
if (this.nicknameError) this.nicknameError = ''; // 输入时清除错误
})
Text(`${this.nickname.length}/20`)
.fontSize(FontSize.CAPTION)
.fontColor(this.nickname.length > 20 ? AppColors.ERROR : AppColors.TEXT_TERTIARY)
.margin({ left: Spacing.SM })
.width(36)
}
三个设计决策:
1. 字数统计用 .length,不用 .trim().length
用户输入空格时应该看到字数在增长。如果输入了 20 个空格,显示 20/20 是合理的——trim() 处理留给提交校验。
2. 输入时清除错误,而非提交时
当输入框边框变红(校验失败)后,用户一旦开始修改内容,错误提示立即消失。这给用户一个清晰的信号:你的修改已被识别,错误状态已解除。
3. 字数超限只在显示层标红,不阻断输入
20 字限制用红色文字提示而不是禁止输入,因为 “禁止输入” 会让用户困惑——明明键盘能打字,为什么输入不进去?红色提示足够醒目,提交时再拦截。
提交时的校验逻辑
private validateNickname(): boolean {
const v = this.nickname.trim();
if (v.length === 0) {
this.nicknameError = '请输入昵称';
return false;
}
if (v.length < 2) {
this.nicknameError = '昵称至少2个字符';
return false;
}
if (v.length > 20) {
this.nicknameError = '昵称最多20个字符';
return false;
}
this.nicknameError = '';
return true;
}
错误信息是递进式的——先判断是否为空,再判断太短,最后判断太长。用户只会看到优先级最高的那个错误,不会同时看到三条。

四、交互点2:密码强度实时检测
这是本文最有技术含量的交互点。密码强度不是等用户输完再判断,而是每次键入都重新计算。
4.1 强度计算算法
private calcStrength(pwd: string): number {
if (pwd.length === 0) return 0; // 空密码
let score = 0;
if (pwd.length >= 6) score++; // 长度加分
if (pwd.length >= 8) score++; // 更长加分
if (/[0-9]/.test(pwd)) score++; // 数字加分
if (/[a-zA-Z]/.test(pwd)) score++;// 字母加分
if (/[^0-9a-zA-Z]/.test(pwd)) score++; // 特殊字符加分
if (score <= 2) return 1; // 弱
if (score <= 3) return 2; // 中
return 3; // 强
}
五个评分维度:长度 ≥6、长度 ≥8、含数字、含字母、含特殊字符。最高 5 分。
| 分数 | 强度 | 颜色 | 示例 |
|---|---|---|---|
| 0 | 空 | 不显示 | (无输入) |
| ≤2 | 弱 | 红色 | 123456 abcdef |
| ≤3 | 中 | 橙色 | abc123 pass12 |
| ≥4 | 强 | 绿色 | Abc@1234 P@ssw0rd! |
4.2 三色强度条
@Builder
StrengthSegment(level: number) {
Row()
.height(4)
.layoutWeight(1)
.backgroundColor(this.passwordStrength >= level ? this.strengthColor() : AppColors.BORDER)
.borderRadius(2)
.margin({ right: level < 3 ? 4 : 0 })
}
三条横线并排,level 从 1 到 3:
- 弱(strength=1):第一段红色,其余灰色
- 中(strength=2):前两段黄色,第三段灰色
- 强(strength=3):三段全绿
视觉上像一个"进度条",直观反映密码强度。
4.3 文字提示
private strengthLabel(): string {
if (this.passwordStrength === 1) return '弱 — 建议增加字母和符号';
if (this.passwordStrength === 2) return '中 — 再增加特殊字符更安全';
return '强 — 密码强度合格';
}
每种强度给出了具体的改进建议,不只是贴标签。用户知道"弱"之后该怎么做——加字母和符号。
五、交互点3:性别选择
没有使用系统 Radio 组件,而是用自定义的按钮组:
@Builder
GenderOption(label: string, value: number) {
Text(label)
.fontSize(FontSize.BODY)
.fontColor(this.gender === value ? Color.White : AppColors.TEXT_SECONDARY)
.backgroundColor(this.gender === value ? AppColors.PRIMARY : Color.White)
.borderRadius(BorderRadius.MD)
.border({ width: 1, color: this.gender === value ? AppColors.PRIMARY : AppColors.BORDER })
.width(80)
.height(44)
.textAlign(TextAlign.Center)
.margin({ right: Spacing.MD })
.onClick(() => {
this.gender = value;
if (this.genderError) this.genderError = '';
})
}
为什么不用系统 Radio?两个原因:
- 系统 Radio 样式难以定制。默认的单选按钮很小,点击区域也小,在移动端体验不好。
- 自定义按钮可以统一设计语言。和页面其他按钮保持一致的圆角、配色、高度。
选中态用"填充 Primary",未选中用"白底灰边框",和前面文章的 Tab 选中态是同一套视觉语言,用户跨页面体验一致。
点击时顺带清除错误信息——用户只要做出了选择,就不需要再提示"请选择性别"。

六、交互点4:协议勾选 + 表单提交
6.1 自定义复选框
和待办事项文章一样的自定义复选框模式:
Text(this.agreed ? '✓' : '')
.fontSize(13)
.fontColor(Color.White)
.width(20).height(20)
.backgroundColor(this.agreed ? AppColors.PRIMARY : Color.Transparent)
.borderRadius(10)
.border({ width: 2, color: this.agreed ? AppColors.PRIMARY : AppColors.TEXT_DISABLED })
.textAlign(TextAlign.Center)
.onClick(() => {
this.agreed = !this.agreed;
if (this.agreedError) this.agreedError = '';
})
复选框 + 协议文字在同一行,点击任一区域都能触发勾选/取消。协议文字本身有一个额外的 onClick——点文字时查看协议详情(Toast 模拟),但不影响勾选行为。
6.2 全局校验 + 模拟提交
private onSubmit(): void {
const v1 = this.validateNickname();
const v2 = this.validateGender();
const v3 = this.validateAgreed();
if (!v1 || !v2 || !v3) return;
this.isSubmitting = true;
setTimeout(() => {
this.isSubmitting = false;
promptAction.showToast({ message: '注册成功!', duration: 2000 });
}, 1500);
}
关键细节:用 || 短路而非 && 逐个执行
以下写法有 bug:
// 错误写法
if (this.validateNickname() && this.validateGender() && this.validateAgreed()) {
// ...
}
&& 是短路运算——如果 validateNickname() 返回 false,后面的 validateGender() 根本不会执行。这意味着用户第一次提交只能看到昵称错误,修复昵称后第二次提交才看到性别错误,再修复性别后第三次提交才看到协议错误。三次提交才能看全所有错误,体验极差。
正确做法是每个校验都执行,用 || 汇总结果:
// 正确写法
const v1 = this.validateNickname();
const v2 = this.validateGender();
const v3 = this.validateAgreed();
if (!v1 || !v2 || !v3) return;
这样所有字段的错误信息同时展示,用户一次看全,一次改完。
提交中状态:isSubmitting 为 true 时按钮文字变为"注册中…",背景变灰,按钮不可点击(防重复提交)。1.5 秒后恢复,显示成功 Toast。
七、校验策略:实时 vs 提交时
表单项校验有两个时机可选,本文做了明确的策略选择:
| 字段 | 实时反馈 | 提交时校验 | 策略 |
|---|---|---|---|
| 昵称 | 字数计数(非阻断) | 是否为空、长度范围 | 实时提示 + 提交拦截 |
| 密码 | 强度条实时更新 | 无额外校验 | 纯实时反馈 |
| 性别 | 无 | 是否已选 | 纯提交校验 |
| 协议 | 无 | 是否已勾选 | 纯提交校验 |
原则:能帮用户变好的给实时反馈,不能帮的只在提交时检查。
密码强度是典型的"实时反馈"场景——用户每敲一个键,都能看到强度条变化,这是一种持续的引导。而性别选择和协议勾选是"是/否"问题,用户要么选要么不选,实时提示没有意义,提交时拦截就够了。
八、代码结构
entry/src/main/ets/
├── common/
│ └── Constants.ets
├── model/
│ ├── ProductModel.ets # 前篇:商品模型
│ ├── FeedModel.ets # 前篇:文章模型
│ └── TodoModel.ets # 前篇:待办模型
└── pages/
├── Index.ets # 入口页(四个按钮)
├── RegisterPage.ets # 本篇核心:注册表单(~220行)
├── ProductListPage.ets # 前篇:商品搜索筛选
├── FeedPage.ets # 前篇:文章Feed分页
└── TodoPage.ets # 前篇:待办CRUD
RegisterPage 约 220 行,没有新增 Model——表单状态完全在组件内部管理,不需要外部数据模型。
九、常见踩坑点
9.1 @State 对象属性修改不触发更新
和待办事项文章的 [...this.todos] 同理,直接修改 this.form.nickname 不触发 UI 更新——this.form 的引用没变。
解决方案:把每个表单字段拆成独立的 @State,或者每次修改都创建新的 form 对象。
// 不触发更新
this.form.nickname = 'new name';
// 触发更新
this.form = { ...this.form, nickname: 'new name' };
扁平 @State 方案彻底规避了这个问题。
9.2 校验顺序的短路问题
前面已经详述——用 || 而非 && 做校验汇总,确保所有字段的错误信息同时展示。
9.3 $$ 双向绑定 vs onChange
// 方式 A:双向绑定 — 代码直接读/写 @State
TextInput({ placeholder: '...', text: $$this.nickname })
// 方式 B:onChange — 手动同步值
TextInput({ placeholder: '...', text: this.nickname })
.onChange((value: string) => { this.nickname = value; })
方式 A 更简洁,适合"输入什么就存什么"的场景。方式 B 适合需要在 onChange 中做额外处理的场景(如格式化、实时校验)。
本文密码字段用 $$ 绑定 + onChange 两者结合——$$ 负责同步值,onChange 负责计算强度。两者不冲突。
9.4 为什么密码没有错误校验?
密码字段没有 passwordError。因为密码是可选的——用户可以只用昵称注册,或者在后续步骤设置密码。如果密码是必填项,应该加上校验。这里的设计是"鼓励填写但不强制",强度条的存在本身就是一种软性引导。
十、运行方式
代码位于 dev/entry/src/main/ets/pages/RegisterPage.ets。
用 DevEco Studio 打开 dev/ 项目,首页点击"注册表单 — 输入校验与状态反馈"即可体验:
- 进入页面 → 空白表单,昵称输入框获取焦点
- 输入昵称 → 实时显示字数 X/20,超过 20 变红
- 输入密码 → 三色强度条实时变化,文字提示改进建议
- 点击性别 → 选中高亮,再次点击切换
- 不填任何信息直接点"注册"→ 三个字段同时显示错误提示
- 逐一修复错误 → 输入过程中错误自动消失
- 全部填完点"注册"→ 按钮变为"注册中…",1.5 秒后提示成功
十一、扩展方向
本文的表单架构可以直接扩展:
- 实时远程校验 — 昵称输入时调用接口检查是否已占用,增加 loading 状态和防抖
- 表单草稿保存 — 用
@ohos.data.preferences定期保存表单数据,防止意外退出丢失 - 多步骤表单 — 拆分成多个步骤(基本信息 → 详细信息 → 确认提交),加进度条
- 富文本校验规则 — 封装一个
Validator类,声明式定义校验规则:{ field: 'nickname', rules: [Required, MinLength(2), MaxLength(20)] } - 键盘处理 — 数字键盘用于手机号、邮箱键盘用于邮箱地址,提升输入体验
- 视觉反馈动效 — 校验失败时输入框抖动动画,密码强度变化时色条平滑过渡
更多推荐




所有评论(0)