鸿蒙ArkUI实战:验证码登录与倒计时按钮
本文介绍了如何使用 ArkUI 实现一个完整的验证码登录页面,包含手机号校验、倒计时按钮、自动验证和定时器管理等核心功能。重点讲解了按钮的四种状态转换逻辑,通过 @State 变量控制倒计时显示,以及输入框的实时校验机制。文章还详细说明了手机号输入框的交互优化、验证码自动校验的实现方式,并强调了定时器清理防止内存泄漏的重要性。整个方案采用状态驱动UI的思路,确保交互流程的可靠性和用户体验的流畅性。
验证码登录是移动端最常见的登录方式。本文用 ArkUI 实现一个完整的验证码登录页面,覆盖手机号校验、倒计时按钮、自动验证和定时器生命周期管理。
一、我们要做什么
一个验证码登录页面,具备完整的交互流程:
- 手机号输入 — 11 位数字输入,+86 前缀,输入过程中实时清除错误
- 获取验证码按钮 — 点击后 60 秒倒计时,期间按钮禁用,文字显示剩余秒数
- 验证码输入 — 6 位数字输入,大字体居中,输满自动触发校验
- 模拟验证 — 正确验证码为 123456,错误提示"验证码错误",清空后可重新输入
- 定时器清理 — 页面销毁时自动清除定时器,防止内存泄漏
这个场景的核心挑战不在 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(写死的模拟值)。真实场景中,后端会校验用户输入的验证码是否匹配发送的验证码。
登录成功后重置 phone 和 code——模拟跳转到主页,页面回到初始状态。

六、定时器生命周期管理
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 会在输满时自动校验,但保留这个按钮有两个原因:
- 如果自动校验失败,用户修改后可以手动点击再试
- 按钮的视觉变化给用户一个反馈——“我已经输入了 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 aboutToDisappear 和 aboutToAppear 的调用时机?
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/ 项目,首页点击"验证码登录 — 倒计时与自动校验"即可体验:
- 进入页面 → 空白表单,"获取验证码"为灰色不可点
- 输入 11 位手机号(以 1 开头)→ 按钮变蓝
- 点击"获取验证码"→ 按钮变为"发送中…",800ms 后 Toast 显示尾号
- 开始 60 秒倒计时 → 按钮显示"60s 后重发"“59s 后重发”…变灰不可点
- 输入验证码 12345 → 底部登录按钮仍为灰色
- 输入第 6 位 → 自动触发校验,按钮变为"验证中…"
- 输入 123456 → Toast"登录成功",表单清空
- 输入错误验证码 → Toast"验证码错误",自动清空验证码输入框
- 倒计时未结束时返回上一页 → 定时器自动清理
十二、扩展方向
- 真实短信发送 — 对接短信服务商 API,把
setTimeout替换为http.createHttp().request() - 语音验证码 — 增加"收不到短信?语音获取"入口,同样的倒计时逻辑
- 图片验证码 — 在发送短信前先要求输入图片验证码,防止机器刷短信
- 滑块验证 — 用拖拽滑块替代图片验证码,体验更流畅
- 自动读取短信 — 利用鸿蒙的短信权限自动提取验证码,省去手动输入
- 密码登录切换 — 增加"密码登录"Tab,在同一页面切换验证码登录和密码登录两种方式
更多推荐

所有评论(0)