Vben-Admin 框架中验证码倒计时动态更新解决方案
一、Vben-Admin 是什么
Vben-Admin是一个基于Vue 3 + TypeScript + Vite的现代化企业级中后台管理系统框架。它提供了:
- 开箱即用的权限管理、路由配置
- 丰富的 UI 组件库(基于 Ant Design Vue 封装)
- 强大的表单、表格组件系统
- 统一的登录/认证模块(
@vben/common-ui)
其中的AuthenticationCodeLogin和VbenPinInput组件,就是框架内置的验证码登录解决方案。
二、为什么会出现这个需求
业务场景
用户登录时通过手机号 + 短信验证码的方式。后端接口有以下逻辑:
- 验证码未过期时→ 不重复发送短信,直接返回已存在的验证码的
expire_at - 验证码已过期或不存在时→ 生成新验证码并发送短信
核心需求
前端倒计时必须与后端验证码的有效期实时同步:
| 场景 | 后端返回expire_at | 期望倒计时 |
|---|---|---|
| 首次请求 | 2026-08-08 14:05:00(5分钟后) | 300 秒 |
| 30 秒后再次请求(验证码未过期) | 2026-08-08 14:05:00(不变) | 270 秒 |
| 4 分 50 秒后再次请求(快过期了) | 2026-08-08 14:05:00(不变) | 10 秒 |
| 5 分钟后再次请求(已过期) | 2026-08-08 14:10:00(新时间) | 300 秒 |
一句话:用户看到的倒计时 = 当前验证码的真实剩余有效时间,每次点击"获取验证码"按钮,都与后端同步校验,无缝续期。
三、遇到的问题
3.1 一开始以为handleSendCode返回值控制倒计时
根据VbenPinInput的 props 定义,我们以为handleSendCode的返回值会控制倒计时秒数:
// ❌ 直觉上以为这样就能控制倒计时handleSendCode:async()=>{// ... 发送验证码 ...return300;// 以为返回 300 就能倒计时 300 秒}3.2 实际测试发现返回无效
当我们测试handleSendCode: async () => { return 20; }时,倒计时仍然显示 60 秒。
3.3 查看组件源码发现真相
通过查看VbenPinInput组件源码,发现其内部逻辑是:
// VbenPinInput 组件内部(简化)const{maxTime=60,// ← 倒计时秒数来自这里!handleSendCode=async()=>{},}=defineProps<PinInputProps>();constcountdown=ref(0);asyncfunctionhandleSend(e:Event){awaithandleSendCode();// ← 只是执行,忽略返回值countdown.value=maxTime;// ← 直接用 maxTime 作为倒计时startCountdown();// ← 每秒减 1}结论:handleSendCode的返回值完全被忽略,倒计时秒数由maxTimeprops 决定。
四、解决方案
4.1 核心思路
利用 Vue 的响应式数据特性,用ref动态绑定maxTime:
// 1. 定义响应式变量,初始 60 秒constmaxTime=ref(60);// 2. 在 handleSendCode 中更新它consthandleSendCode=async()=>{constres=awaitsendCodeApi({phone});if(res.data.expire_at){constseconds=getSecondsRemaining(res.data.expire_at);maxTime.value=seconds>0?seconds:60;// ✅ 更新倒计时}};// 3. 传给 VbenPinInputcomponentProps:{maxTime:maxTime,// ← 响应式绑定,值变了组件自动更新}4.2 完整代码
<script lang="ts" setup> import { computed, ref } from 'vue'; import { AuthenticationCodeLogin, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { message } from 'ant-design-vue'; import { sendCodeApi } from '#/api'; // 1. 响应式倒计时秒数 const maxTime = ref(60); // 2. 计算剩余秒数 const getSecondsRemaining = (dateString: string) => { const diffMs = new Date(dateString).getTime() - new Date().getTime(); return Math.floor(diffMs / 1000); }; // 3. 发送验证码 const handleSendCode = async () => { try { const res = await sendCodeApi({ phone: currentPhone }); if (res.code === 0) { if (res.data.expire_at) { const seconds = getSecondsRemaining(res.data.expire_at); // ✅ 关键:更新响应式变量 maxTime.value = seconds > 0 ? seconds : 60; } } } catch (error) { // 错误处理... } }; // 4. 表单配置 const formSchema = computed((): VbenFormSchema[] => [ { component: 'VbenInput', fieldName: 'phone', // ... }, { component: 'VbenPinInput', fieldName: 'code', componentProps: { codeLength: 6, maxTime: maxTime, // ✅ 响应式绑定 createText: (countdown: number) => { return countdown > 0 ? `${countdown} 秒后重发` : '获取验证码'; }, handleSendCode: handleSendCode, }, // ... }, ]); </script>4.3 数据流图
用户点击按钮 │ ▼ handleSendCode() │ ▼ sendCodeApi() → 后端返回 { expire_at } │ ▼ getSecondsRemaining(expire_at) → 计算剩余秒数 │ ▼ maxTime.value = 剩余秒数 ← 响应式更新 │ ▼ VbenPinInput 内部 countdown.value = maxTime ← 组件收到新值 │ ▼ 倒计时按新的秒数开始运行五、总结
| 项目 | 内容 |
|---|---|
| 框架 | Vben-Admin 的@vben/common-ui认证模块 |
| 组件 | VbenPinInput(验证码输入+倒计时) |
| 误区 | 误以为handleSendCode返回值控制倒计时 |
| 真相 | maxTimeprops 才是倒计时秒数的唯一来源 |
| 解法 | 用ref响应式绑定maxTime,在handleSendCode中动态更新 |
| 效果 | 倒计时始终与后端expire_at保持同步 |