任何应用都离不开表单——注册登录、意见反馈、资料编辑。本文用 ArkUI 构建一个完整的注册表单,覆盖字符计数、密码强度检测、自定义单选、协议勾选和提交校验,并深入讲解表单校验的策略选择。


一、我们要做什么

一个注册页面,具备完整的表单交互能力:

  1. 昵称输入 + 字符计数 — 实时显示 X/20 字,超限变红提示
  2. 密码输入 + 强度检测 — 输入时实时计算密码强度,三色进度条 + 文字提示
  3. 性别选择 — 自定义单选按钮组,选中高亮填充
  4. 协议勾选 + 提交校验 — 全字段校验,空字段精准报错,模拟提交 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?两个原因:

  1. 系统 Radio 样式难以定制。默认的单选按钮很小,点击区域也小,在移动端体验不好。
  2. 自定义按钮可以统一设计语言。和页面其他按钮保持一致的圆角、配色、高度。

选中态用"填充 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;

这样所有字段的错误信息同时展示,用户一次看全,一次改完。

提交中状态isSubmittingtrue 时按钮文字变为"注册中…",背景变灰,按钮不可点击(防重复提交)。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/ 项目,首页点击"注册表单 — 输入校验与状态反馈"即可体验:

  1. 进入页面 → 空白表单,昵称输入框获取焦点
  2. 输入昵称 → 实时显示字数 X/20,超过 20 变红
  3. 输入密码 → 三色强度条实时变化,文字提示改进建议
  4. 点击性别 → 选中高亮,再次点击切换
  5. 不填任何信息直接点"注册"→ 三个字段同时显示错误提示
  6. 逐一修复错误 → 输入过程中错误自动消失
  7. 全部填完点"注册"→ 按钮变为"注册中…",1.5 秒后提示成功

十一、扩展方向

本文的表单架构可以直接扩展:

  • 实时远程校验 — 昵称输入时调用接口检查是否已占用,增加 loading 状态和防抖
  • 表单草稿保存 — 用 @ohos.data.preferences 定期保存表单数据,防止意外退出丢失
  • 多步骤表单 — 拆分成多个步骤(基本信息 → 详细信息 → 确认提交),加进度条
  • 富文本校验规则 — 封装一个 Validator 类,声明式定义校验规则:{ field: 'nickname', rules: [Required, MinLength(2), MaxLength(20)] }
  • 键盘处理 — 数字键盘用于手机号、邮箱键盘用于邮箱地址,提升输入体验
  • 视觉反馈动效 — 校验失败时输入框抖动动画,密码强度变化时色条平滑过渡
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐