【实战分享】Vue3组合式函数useCountDown同一个页面中多个验证码倒计时的实现

  • 原创
  • 作者:程序员三丰
  • 发布时间:2025-08-01 10:59
  • 浏览量:204
本文将以代码实例的形式分享解决之前分享的获取验证码倒计时组合式函数的应用场景之:同一个页面中多个获取验证码倒计时模块的实现。

引子

之前分享了一篇文章《【实战分享】Vue3组合式函数封装:获取验证码倒计时》,是日常工作中自己封装的一个快速实现获取验证码倒计时的交互的组合式函数,核心思路是只关注交互与数据的实现,不涉及UI,目的就是更简单更灵活,毕竟现在前端UI框架琳琅满目,组合式函数的形式更容易接入到三方框架或者自己项目代码中。

一般我们是单个页面来使用它,比如登录页、注册页等,就会容易很多。有需要可以参考这篇文章《【实战分享】分享一个完整的Vue3+Element Plus的手机验证码登录页面源码》这是一个比较完整的应用示例。

但是,有些特殊的场景,会要求同一个页面中存在多个获取验证码倒计时,比如一个页面中左侧是短信验证登录,右侧是邮箱验证登录,如果还是按照常规思路的话,那么这个获取倒计时的模块就不能正常工作了,一个运行,另外的也会同时更新状态(可以通俗理解为状态数据交叉或共享,实际上是数据污染了)。

那如何解决这个问题,下文将理论结合实践给出解决方案。

效果截图

在线DEMO:https://lba.51blog.xyz/#/demos/countDownMultiple

解决思路

其实解决问题的关键在以下两点:

1、理解Vue组件的状态管理的原理

简单讲就是,默认每一个Vue组件实例都独立管理自己的响应式状态,互不影响。

2、理解什么Vue中的组合式函数

与普通函数对比,Vue中组合式函数(Composables)是可以利用Vue的组合式API来封装和复用有状态逻辑的函数。

特别之处,就在于每一个调用组合式函数的组件实例都会创建其独有的组合式函数内部定义的状态拷贝,因此他们不会互相影响,这也是我们解决本文开头提出的问题关键所在。

如果你还不了解什么是“组合式函数”,请移步官方文档:

https://cn.vuejs.org/guide/reusability/composables.html

如果你理解了上面2点,那么就更容易理解下面我讲的具体解决问题的思路:就是把每个使用获取验证码倒计时的模块独立定义为一个单文件的组件以复用,然后需要应用的页面中引入即可。

结合上面效果截图,就是把左右两侧的手机短信验证和邮箱验证分别封装成单文件组件,然后在各自的组件文件中正常使用获取短信验证码组合式函数即可。

代码实践

代码文件目录结构

以下只是推荐的目录结构,实践的时候请根据实际情况调整。

src
  - lang
    - demos
      - en
      - zh-cn
        - countDownMultile.ts
  - views
      - demos
        - countDownMultiple
          - emailValidateForm.vue
          - index.vue
          - mobileValidateForm.vue

组件代码封装

手机短信验证组件:src/views/demos/countDownMultiple/mobileValidateForm.vue

<template>
  <div>
    <h2>{{ t('demos.countDownMultiple.Vaidate form title for mobile') }}</h2>
    <el-form ref="formRef" :model="formModel" :rules="formRules">
      <el-form-item prop="phone">
        <el-input
          size="large"
          v-model="formModel.phone"
          clearable
          type="number"
          :placeholder="t('Please input field', { field: t('utils.mobile') })"
          @input="onPhoneInput"
          />
      </el-form-item>
      <el-form-item prop="code">
        <el-row style="width: 100%">
          <el-col :span="10">
            <el-input size="large" v-model="formModel.code" clearable type="number" @input="onCodeInput" :placeholder="t('utils.code')" />
          </el-col>
          <el-col :span="14" style="padding-left: 10px">
            <el-button size="large" type="primary" plain style="width: 100%" :disabled="countDownWorking" @click="onGetCodeClick">
              {{ countDownText }}
            </el-button>
          </el-col>
        </el-row>
      </el-form-item>
      <el-button type="primary" size="large" round class="submit-btn" @click="submit(formRef)">
        {{ t('Confirm') }}
      </el-button>
    </el-form>
  </div>
</template>

<script setup lang="ts">
  import { useI18n } from 'vue-i18n'
  import { ref, reactive } from 'vue'
  import type { FormInstance, FormRules } from 'element-plus'
  import { useCountDown } from '@/utils/countDown'

  const { startCountDown, countDownText, countDownWorking, showLog } = useCountDown()

  showLog.value = true

  const onGetCodeClick = () => {
    formRef
      .value!.validateField('phone')
      .then((valid) => {
        if (valid) {
          startCountDown()
        }
      })
      .catch(() => {})
  }

  interface FormModelType {
    phone: string
    code: string
  }

  const { t } = useI18n()
  const formRef = ref<FormInstance>()
  const formModel = reactive<FormModelType>({
    phone: '',
    code: '',
  })

  const checkPhone = (rule: any, value: any, callback: any) => {
    if (!value || value.trim() === '') {
      callback(new Error(t('Please input field', { field: t('utils.mobile') })))
    } else if (!/^(1[3-9])\d{9}$/.test(value.toString())) {
      callback(new Error(t('utils.Mobile format incorrect')))
    } else {
      callback()
    }
  }

  const formRules = reactive<FormRules<FormModelType>>({
    phone: [{ validator: checkPhone }],
    code: [{ required: true, message: t('Please input field', { field: t('utils.code') }) }],
  })

  const submit = async (formEl: FormInstance | undefined) => {
    if (!formEl) return

    await formEl.validate((valid, fields) => {
      if (valid) {
        console.log('submit')
      } else {
        console.log('error submit', fields)
      }
    })
  }

const onPhoneInput = (value: string) => {
    formModel.phone = value.slice(0, 11)
}

const onCodeInput = (value: string) => {
    formModel.code = value.slice(0, 6)
}
</script>

<style scoped lang="scss">
h2 {
    position: relative;
    color: var(--el-color-primary);
    text-align: center;
    margin-bottom: 64px;
    letter-spacing: 10px;
    padding-left: 10px;

    &::after {
        content: '';
        position: absolute;
        bottom: -16px;
        left: 41%;
        width: 18%;
        height: 2px;
        background-color: var(--el-color-primary);
    }
}

:deep(.el-form-item) {
    margin-bottom: 24px;
}

.submit-btn {
    margin-top: 10px;
    width: 100%;
    :deep(span) {
        letter-spacing: 12px;
        margin-left: 12px; // 解决letter-spacing导致按钮文本不居中的问题
    }
}
</style>

邮箱验证组件:src/views/demos/countDownMultiple/emailValidateForm.vue

<template>
    <div>
        <h2>{{ t('demos.countDownMultiple.Vaidate form title for email') }}</h2>
        <el-form ref="formRef" :model="formModel" :rules="formRules">
            <el-form-item prop="email">
                <el-input size="large" v-model="formModel.email" clearable :placeholder="t('Please input field', { field: t('utils.email') })" />
            </el-form-item>
            <el-form-item prop="code">
                <el-row style="width: 100%">
                    <el-col :span="10">
                        <el-input size="large" v-model="formModel.code" clearable type="number" @input="onCodeInput" :placeholder="t('utils.code')" />
                    </el-col>
                    <el-col :span="14" style="padding-left: 10px">
                        <el-button size="large" type="primary" plain style="width: 100%" :disabled="countDownWorking" @click="onGetCodeClick">
                            {{ countDownText }}
                        </el-button>
                    </el-col>
                </el-row>
            </el-form-item>
            <el-button type="primary" size="large" round class="submit-btn" @click="submit(formRef)">
                {{ t('Confirm') }}
            </el-button>
        </el-form>
    </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useCountDown } from '@/utils/countDown'

const { startCountDown, countDownText, countDownWorking, showLog } = useCountDown()

showLog.value = true

const onGetCodeClick = () => {
    formRef
        .value!.validateField('email')
        .then((valid) => {
            if (valid) {
                startCountDown()
            }
        })
        .catch(() => {})
}

interface FormModelType {
    email: string
    code: string
}

const { t } = useI18n()
const formRef = ref<FormInstance>()
const formModel = reactive<FormModelType>({
    email: '',
    code: '',
})

const formRules = reactive<FormRules<FormModelType>>({
    email: [
        { required: true, message: t('Please input field', { field: t('utils.email') }) },
        { type: 'email', message: t('utils.Email format incorrect') },
    ],
    code: [{ required: true, message: t('Please input field', { field: t('utils.code') }) }],
})

const submit = async (formEl: FormInstance | undefined) => {
    if (!formEl) return

    await formEl.validate((valid, fields) => {
        if (valid) {
            console.log('submit')
        } else {
            console.log('error submit', fields)
        }
    })
}

const onCodeInput = (value: string) => {
    formModel.code = value.slice(0, 6)
}
</script>

<style scoped lang="scss">
h2 {
    position: relative;
    color: var(--el-color-primary);
    text-align: center;
    margin-bottom: 64px;
    letter-spacing: 10px;
    padding-left: 10px;

    &::after {
        content: '';
        position: absolute;
        bottom: -16px;
        left: 41%;
        width: 18%;
        height: 2px;
        background-color: var(--el-color-primary);
    }
}

:deep(.el-form-item) {
    margin-bottom: 24px;
}

.submit-btn {
    margin-top: 10px;
    width: 100%;
    :deep(span) {
        letter-spacing: 12px;
        margin-left: 12px; // 解决letter-spacing导致按钮文本不居中的问题
    }
}
</style>

组件引用集成

语言包:src/lang/demos/zh-cn/countDownMultile.ts(注意:语言包的引用请根据自己项目的实际情况自行调整

export default {
  countDownMultiple: {
    'Vaidate form title for mobile': '手机短信验证',
    'Vaidate form title for email': '邮箱验证',
    'Please input field': '请输入{field}',
    utils: {
      mobile: '手机号',
      'Mobile format incorrect': '手机号格式错误',
      email: '邮箱地址',
      'Email format incorrect': '邮箱地址格式错误',
      code: '验证码',
    },
    Confirm: '确认',
  },
}

引用集成:src/views/demos/countDownMultiple/index.vue

<template>
    <div class="page-container">
        <div class="page-container-content">
            <div class="left">
                <MobileValidateForm />
            </div>
            <div class="right">
                <EmailValidateForm />
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import MobileValidateForm from './mobileValidateForm.vue'
import EmailValidateForm from './emailValidateForm.vue'
</script>

<style scoped lang="scss">
.page-container {
    background: #f5f7fa;
    height: calc(100vh - 0px);
    padding: 10px 0;

    .page-container-content {
        background: #fff;
        width: 86%;
        height: 100%;
        margin: 0 auto;
        position: relative;
        display: flex;
        justify-content: center;

        & > div {
            width: 50%;
            flex: 1 1 auto;

            &.left {
                border-right: 0.5px solid var(--el-color-info-light-7);
            }

            display: flex;
            justify-content: center;
            align-items: center;

            & > div {
                width: 60%;
                flex: none;
            }
        }
    }
}

@media screen and (max-width: 1280px) {
    .page-container-content {
        flex-direction: column;
        & > div {
            width: 100% !important;
        }

        & > .left {
            border: none !important;
            border-bottom: 0.5px solid var(--el-color-info-light-7) !important;
        }
    }
}
@media screen and (max-width: 375px) {
}
@media screen and (max-height: 650px) {
}
@at-root html.dark {
}
</style>

尾语

感谢阅读,以上便是本文所有内容,希望对您有所帮助!

声明:本文为原创文章,51blog.xyz和作者拥有版权,如需转载,请注明来源于51blog.xyz并保留原文链接:https://www.51blog.xyz/article/92.html

文章归档

推荐文章

buildadmin logo
Thinkphp8 Vue3 Element PLus TypeScript Vite Pinia

🔥BuildAdmin是一个永久免费开源,无需授权即可商业使用,且使用了流行技术栈快速创建商业级后台管理系统。

热门标签

PHP ThinkPHP ThinkPHP5.1 Go Mysql Mysql5.7 Redis Linux CentOS7 Git HTML CSS CSS3 Javascript JQuery Vue LayUI VMware Uniapp 微信小程序 docker wiki Confluence7 学习笔记 uView ES6 Ant Design Pro of Vue React ThinkPHP6.0 chrome 扩展 翻译工具 Nuxt SSR 服务端渲染 scrollreveal.js ThinkPHP8.0 Mac webman 跨域CORS vscode GitHub ECharts Canvas vue3