验证码登录是移动端最常见的登录方式。本文用 ArkUI 实现一个完整的验证码登录页面,覆盖手机号校验、倒计时按钮、自动验证和定时器生命周期管理。


一、我们要做什么

一个验证码登录页面,具备完整的交互流程:

  1. 手机号输入 — 11 位数字输入,+86 前缀,输入过程中实时清除错误
  2. 获取验证码按钮 — 点击后 60 秒倒计时,期间按钮禁用,文字显示剩余秒数
  3. 验证码输入 — 6 位数字输入,大字体居中,输满自动触发校验
  4. 模拟验证 — 正确验证码为 123456,错误提示"验证码错误",清空后可重新输入
  5. 定时器清理 — 页面销毁时自动清除定时器,防止内存泄漏

这个场景的核心挑战不在 UI 布局,而在定时器状态管理按钮多状态切换


二、按钮的四种状态

"获取验证码"按钮是整个页面状态最复杂的组件。它有四种互斥的状态:

状态 条件 文字 样式
可点击 手机号合法 + 未倒计时 获取验证码 蓝色填充
发送中 正在请求发送 发送中… 灰色,不可点击
倒计时 已发送,60s 内 XXs 后重发 灰色,不可点击
不可点击 手机号不合法 获取验证码 灰色,不可点击

四个状态通过两个变量控制:

@State countdown: number = 0;     // 0=可点击, >0=倒计时中
@State isSending: boolean = false; // 是否正在请求发送

加上手机号合法性判断 isPhoneValid(),三个条件决定按钮行为:

private codeButtonEnabled(): boolean {
  return this.isPhoneValid() && this.countdown === 0 && !this.isSending;
}

private codeButtonText(): string {
  if (this.isSending) return '发送中...';
  if (this.countdown > 0) return `${this.countdown}s 后重发`;
  return '获取验证码';
}

这种"多重条件 → 派生文案/样式"的模式,比用 if/else 分支管理更可靠——所有状态变化都经过同一套规则,不会出现"按钮数字在减但还能点"的 bug。


在这里插入图片描述

三、交互点1:手机号输入与校验

Row() {
  Text('+86')
    .fontSize(FontSize.BODY)
    .fontColor(AppColors.TEXT_SECONDARY)
    .fontWeight(FontWeight.Medium)
  Row()
    .width(1).height(18)
    .backgroundColor(AppColors.BORDER)
    .margin({ left: Spacing.MD, right: Spacing.MD })
  TextInput({ placeholder: '请输入手机号', text: $$this.phone })
    .type(InputType.Number)
    .maxLength(11)
    .fontSize(FontSize.BODY)
    .layoutWeight(1)
    .height(44)
    .backgroundColor(Color.Transparent)
    .onChange(() => {
      if (this.phoneError) this.phoneError = '';  // 输入时清除错误
    })
}
.width('100%')
.height(52)
.backgroundColor(Color.White)
.borderRadius(BorderRadius.MD)
.border({ width: 1, color: this.phoneError ? AppColors.ERROR : AppColors.BORDER })

设计细节:

1. 输入框无边框 + 外层 Row 统一边框

TextInput 设置 backgroundColor(Color.Transparent) 和默认无边框,视觉边框由外层 Row 提供。这样 +86 前缀和输入框在同一个白色圆角框内,看起来是一个整体。如果两部分各有各的边框,视觉上就是两个独立组件,不自然。

2. 用 InputType.Number 弹出数字键盘

type(InputType.Number) 让系统弹出数字键盘而非全键盘。手机号是纯数字,用数字键盘提升输入效率,也避免用户输入字母。

3. 校验时机是点击"获取验证码"时,而非 onChange

不实时校验——用户输入到第 5 位数字时不打断他。只在点击按钮时做最终校验,失败时显示错误并标红边框。

4. 输入时自动清除错误

onChange 中清除错误状态,边框从红色恢复灰色。用户一旦开始修改,就给他一个"错误已解除"的信号。


在这里插入图片描述

四、交互点2:倒计时按钮(setInterval 实战)

这是本文最核心的交互。点击按钮 → 发送验证码 → 启动 60 秒倒计时:

private sendCode(): void {
  if (!this.isPhoneValid()) {
    this.phoneError = '请输入正确的11位手机号';
    return;
  }
  this.phoneError = '';
  this.isSending = true;

  // 模拟网络请求发送验证码
  setTimeout(() => {
    this.isSending = false;
    this.startCountdown();
    promptAction.showToast({
      message: '验证码已发送至尾号 ' + this.phone.slice(-4),
      duration: 1500
    });
  }, 800);
}

isSending = true 期间,按钮显示"发送中…"且不可点击——防止用户在请求进行中重复点击。

private startCountdown(): void {
  this.countdown = 60;
  this.timerId = setInterval(() => {
    this.countdown--;
    if (this.countdown <= 0) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }, 1000);
}

setInterval 每秒执行一次:

  • this.countdown-- 让数字从 60 递减到 0
  • 减到 0 时 clearInterval 停止定时器
  • 按钮文字自动从 “60s 后重发” 变回 “获取验证码”

为什么 countdown 要用 @State

因为按钮文字依赖 countdown 的值。@State 保证每次 countdown 变化时,build() 重新执行,按钮文字随之更新。如果 countdown 是普通 private 变量,定时器修改了它但 UI 不知道,倒计时数字永远不会变。

Toast 显示尾号this.phone.slice(-4) 取手机号最后 4 位。用户收到短信时,看到页面上写着"发送至尾号 5678",确认发送到了正确的号码。这是一个提升信任感的小细节。


五、交互点3:验证码自动校验

TextInput({ placeholder: '输入6位验证码', text: $$this.code })
  .type(InputType.Number)
  .maxLength(6)
  .fontSize(FontSize.TITLE)
  .fontWeight(FontWeight.Bold)
  .layoutWeight(1)
  .height(44)
  .backgroundColor(Color.Transparent)
  .onChange(() => {
    if (this.code.length === 6) {
      this.verifyCode();  // 输入第6位时自动触发
    }
  })

用户不需要额外点击"提交"按钮——6 位验证码输满后自动触发校验。这比传统"输入完再点提交"少一步操作,体验更流畅。

maxLength(6) 确保用户不能输入超过 6 位数字。

校验逻辑

private verifyCode(): void {
  if (this.code.length !== 6) return;    // 防护
  if (this.isVerifying) return;          // 防重复提交

  this.isVerifying = true;
  setTimeout(() => {
    this.isVerifying = false;
    if (this.code === '123456') {
      promptAction.showToast({ message: '登录成功!', duration: 2000 });
      this.phone = '';
      this.code = '';
    } else {
      promptAction.showToast({ message: '验证码错误,请重新输入', duration: 2000 });
      this.code = '';  // 清空让用户重新输入
    }
  }, 1200);
}

三个防护点:

  • code.length !== 6 — 确保只在满 6 位时执行(手动点击按钮时也可能触发)
  • isVerifying — 防止验证过程中重复提交
  • 验证失败清空 this.code = '' — 让用户重新输入,不需要手动删除

正确验证码为 123456(写死的模拟值)。真实场景中,后端会校验用户输入的验证码是否匹配发送的验证码。

登录成功后重置 phonecode——模拟跳转到主页,页面回到初始状态。


在这里插入图片描述

六、定时器生命周期管理

setInterval 创建了一个持续运行的定时器。如果用户在这个页面倒计时到 30 秒时按返回键退出,定时器还在跑——每秒执行一次,直到 30 秒后才停。

对于短倒计时(60 秒),多跑 30 秒影响不大。但如果是更长的定时器,或者多次进出页面创建多个定时器,就会造成内存泄漏——定时器持有组件引用,组件销毁了但定时器还在,JS 引擎不能回收内存。

解决方案:aboutToDisappear 清理

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

aboutToDisappear 是 ArkUI 的生命周期回调,在组件即将从页面栈中移除时调用。在这里清除定时器,确保不会有多余的后台任务继续运行。

检查 timerId !== -1 是因为定时器可能还没启动(用户没点"获取验证码"就退出了),或者已经自然结束了(60 秒已过)。不检查的话 clearInterval(-1) 不会有实际影响,但是一种防御性编程。


七、底部登录按钮

除了自动校验,还有一个底部"登录"按钮供用户手动触发:

Button(this.isVerifying ? '验证中...' : '登录')
  .fontSize(FontSize.MEDIUM)
  .fontColor(Color.White)
  .backgroundColor(this.code.length === 6 ? AppColors.PRIMARY : AppColors.TEXT_DISABLED)
  .borderRadius(BorderRadius.MD)
  .width('100%')
  .height(48)
  .enabled(this.code.length === 6 && !this.isVerifying)
  .onClick(() => this.verifyCode())

按钮有两种视觉状态:

  • 已输入 6 位:蓝色填充,可点击
  • 未满 6 位:灰色,不可点击

虽然 onChange 会在输满时自动校验,但保留这个按钮有两个原因:

  1. 如果自动校验失败,用户修改后可以手动点击再试
  2. 按钮的视觉变化给用户一个反馈——“我已经输入了 6 位,可以提交了”

八、代码结构

entry/src/main/ets/
├── common/
│   └── Constants.ets
├── model/                    # 本篇无新增 Model(状态全在组件内)
└── pages/
    ├── Index.ets             # 入口页(七个按钮)
    ├── VerifyCodePage.ets    # 本篇核心:验证码登录(~180行)
    ├── HomePage.ets          # 前篇:首页轮播
    ├── ProfilePage.ets       # 前篇:个人中心
    ├── RegisterPage.ets      # 前篇:注册表单
    ├── TodoPage.ets          # 前篇:待办CRUD
    ├── FeedPage.ets          # 前篇:文章分页
    └── ProductListPage.ets   # 前篇:商品搜索

VerifyCodePage 约 180 行,无新增数据模型。


九、状态流转图

初始状态
  手机号:空        按钮:获取验证码(灰)
  验证码:空        登录:登录(灰)
    ↓
输入 11 位手机号
  手机号:138xxxx   按钮:获取验证码(蓝,可点)
    ↓
点击"获取验证码"
  按钮:发送中... → 60s 后重发 → 59s 后重发 → ...
  Toast:验证码已发送至尾号 xxxx
    ↓
输入验证码
  验证码:12345     登录:登录(灰)
    ↓
输入第 6 位
  验证码:123456    登录:登录(蓝,可点)
  自动触发校验
    ↓
校验中 → 1.2s
  ├─ 正确(123456) → Toast"登录成功" → 清空表单
  └─ 错误 → Toast"验证码错误" → 清空验证码

十、常见面试题 / 踩坑点

10.1 setInterval 里的 this 指向问题

JavaScript 中 setInterval(() => { ... }) 使用箭头函数,this 指向定义时的上下文(即组件实例)。ArkTS 也遵循这个规则,所以 this.countdown 能正确访问组件状态。

如果用普通函数 setInterval(function() { ... })this 会指向全局对象或 undefined,访问不到组件属性。

10.2 为什么 countdown 要用 @State

定时器每秒修改 this.countdown 的值。如果它是普通变量,修改后 UI 不会重绘,按钮上的倒计时数字永远不会变。@State 让框架追踪这个变量的引用变化,每次修改都触发按钮所在区域的重绘。

10.3 为什么要检查 isVerifying

用户快速点击"登录"按钮两次(或自动校验 + 手动点击同时触发),会导致 verifyCode() 被执行两次,两次 setTimeout 在 1.2 秒后都执行。如果第一次返回"成功"、第二次返回"错误"——用户看到"验证码错误"但明明刚才成功了。isVerifying 锁防止这个情况。

10.4 aboutToDisappearaboutToAppear 的调用时机?

  • aboutToAppear() — 页面创建后、build() 执行前调用
  • aboutToDisappear() — 页面被 router.back() 或用户按返回键时,组件从页面栈移除前调用

aboutToDisappear 中清理定时器、取消网络请求、移除事件监听——任何需要在组件销毁时停止的异步操作。

10.5 倒计时用 setInterval 还是 setTimeout 链?

两种方式都能实现:

// 方式 A:setInterval(本文)
this.timerId = setInterval(() => {
  this.countdown--;
  if (this.countdown <= 0) clearInterval(this.timerId);
}, 1000);

// 方式 B:setTimeout 链
private tick(): void {
  this.countdown--;
  if (this.countdown > 0) {
    this.timerId = setTimeout(() => this.tick(), 1000);
  }
}

方式 A 代码更短,但存在"跳秒"风险——如果 JS 线程繁忙,setInterval 可能连续执行两次回调导致一下跳 2 秒。方式 B 更精确,每次执行完再排下一个。对于 60 秒的 UI 倒计时来说,方式 A 的误差(偶尔跳 1 秒)完全可接受——用户感知不到,且代码量少一半。


十一、运行方式

代码位于 dev/entry/src/main/ets/pages/VerifyCodePage.ets

用 DevEco Studio 打开 dev/ 项目,首页点击"验证码登录 — 倒计时与自动校验"即可体验:

  1. 进入页面 → 空白表单,"获取验证码"为灰色不可点
  2. 输入 11 位手机号(以 1 开头)→ 按钮变蓝
  3. 点击"获取验证码"→ 按钮变为"发送中…",800ms 后 Toast 显示尾号
  4. 开始 60 秒倒计时 → 按钮显示"60s 后重发"“59s 后重发”…变灰不可点
  5. 输入验证码 12345 → 底部登录按钮仍为灰色
  6. 输入第 6 位 → 自动触发校验,按钮变为"验证中…"
  7. 输入 123456 → Toast"登录成功",表单清空
  8. 输入错误验证码 → Toast"验证码错误",自动清空验证码输入框
  9. 倒计时未结束时返回上一页 → 定时器自动清理

十二、扩展方向

  • 真实短信发送 — 对接短信服务商 API,把 setTimeout 替换为 http.createHttp().request()
  • 语音验证码 — 增加"收不到短信?语音获取"入口,同样的倒计时逻辑
  • 图片验证码 — 在发送短信前先要求输入图片验证码,防止机器刷短信
  • 滑块验证 — 用拖拽滑块替代图片验证码,体验更流畅
  • 自动读取短信 — 利用鸿蒙的短信权限自动提取验证码,省去手动输入
  • 密码登录切换 — 增加"密码登录"Tab,在同一页面切换验证码登录和密码登录两种方式
Logo

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

更多推荐