From c91dd282a623a2cceaa27c628835d0962a657c7e Mon Sep 17 00:00:00 2001 From: Soybean Date: Sun, 24 Mar 2024 04:03:49 +0800 Subject: [PATCH] feat(projects): login page: code-login --- packages/hooks/src/index.ts | 3 +- packages/hooks/src/use-count-down.ts | 49 +++++++++++++ src/hooks/business/captcha.ts | 71 +++++++++++++++++++ src/locales/langs/en-us.ts | 4 +- src/locales/langs/zh-cn.ts | 4 +- src/typings/app.d.ts | 2 + .../_builtin/login/modules/code-login.vue | 46 ++++++++++-- .../_builtin/login/modules/pwd-login.vue | 2 +- 8 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 packages/hooks/src/use-count-down.ts create mode 100644 src/hooks/business/captcha.ts diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 2ea9a522..a36c8436 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,9 +1,10 @@ import useBoolean from './use-boolean'; import useLoading from './use-loading'; +import useCountDown from './use-count-down'; import useContext from './use-context'; import useSvgIconRender from './use-svg-icon-render'; import useHookTable from './use-table'; -export { useBoolean, useLoading, useContext, useSvgIconRender, useHookTable }; +export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable }; export * from './use-table'; diff --git a/packages/hooks/src/use-count-down.ts b/packages/hooks/src/use-count-down.ts new file mode 100644 index 00000000..bfad064b --- /dev/null +++ b/packages/hooks/src/use-count-down.ts @@ -0,0 +1,49 @@ +import { computed, onScopeDispose, ref } from 'vue'; +import { useRafFn } from '@vueuse/core'; + +/** + * count down + * + * @param seconds - count down seconds + */ +export default function useCountDown(seconds: number) { + const FPS_PER_SECOND = 60; + + const fps = ref(0); + + const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND)); + + const isCounting = computed(() => fps.value > 0); + + const { pause, resume } = useRafFn( + () => { + if (fps.value > 0) { + fps.value -= 1; + } else { + pause(); + } + }, + { immediate: false } + ); + + function start(updateSeconds: number = seconds) { + fps.value = FPS_PER_SECOND * updateSeconds; + resume(); + } + + function stop() { + fps.value = 0; + pause(); + } + + onScopeDispose(() => { + pause(); + }); + + return { + count, + isCounting, + start, + stop + }; +} diff --git a/src/hooks/business/captcha.ts b/src/hooks/business/captcha.ts new file mode 100644 index 00000000..c2303d31 --- /dev/null +++ b/src/hooks/business/captcha.ts @@ -0,0 +1,71 @@ +import { computed } from 'vue'; +import { useCountDown, useLoading } from '@sa/hooks'; +import { $t } from '@/locales'; +import { REG_PHONE } from '@/constants/reg'; + +export function useCaptcha() { + const { loading, startLoading, endLoading } = useLoading(); + const { count, start, stop, isCounting } = useCountDown(10); + + const label = computed(() => { + let text = $t('page.login.codeLogin.getCode'); + + const countingLabel = $t('page.login.codeLogin.reGetCode', { time: count.value }); + + if (loading.value) { + text = ''; + } + + if (isCounting.value) { + text = countingLabel; + } + + return text; + }); + + function isPhoneValid(phone: string) { + if (phone.trim() === '') { + window.$message?.error?.($t('form.phone.required')); + + return false; + } + + if (!REG_PHONE.test(phone)) { + window.$message?.error?.($t('form.phone.invalid')); + + return false; + } + + return true; + } + + async function getCaptcha(phone: string) { + const valid = isPhoneValid(phone); + + if (!valid || loading.value) { + return; + } + + startLoading(); + + // request + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + + window.$message?.success?.($t('page.login.codeLogin.sendCodeSuccess')); + + start(); + + endLoading(); + } + + return { + label, + start, + stop, + isCounting, + loading, + getCaptcha + }; +} diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 53079141..4e1af859 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -184,6 +184,8 @@ const local: App.I18n.Schema = { codeLogin: { title: 'Verification Code Login', getCode: 'Get verification code', + reGetCode: 'Reacquire after {time}s', + sendCodeSuccess: 'Verification code sent successfully', imageCodePlaceholder: 'Please enter image verification code' }, register: { @@ -391,7 +393,7 @@ const local: App.I18n.Schema = { }, pwd: { required: 'Please enter password', - invalid: 'Password format is incorrect' + invalid: '6-18 characters, including letters, numbers, and underscores' }, confirmPwd: { required: 'Please enter password again', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 9d61bfa5..af45fc03 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -184,6 +184,8 @@ const local: App.I18n.Schema = { codeLogin: { title: '验证码登录', getCode: '获取验证码', + reGetCode: '{time}秒后重新获取', + sendCodeSuccess: '验证码发送成功', imageCodePlaceholder: '请输入图片验证码' }, register: { @@ -391,7 +393,7 @@ const local: App.I18n.Schema = { }, pwd: { required: '请输入密码', - invalid: '密码格式不正确' + invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线' }, confirmPwd: { required: '请输入确认密码', diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index f09d15cc..8bd0246e 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -367,6 +367,8 @@ declare namespace App { codeLogin: { title: string; getCode: string; + reGetCode: string; + sendCodeSuccess: string; imageCodePlaceholder: string; }; register: { diff --git a/src/views/_builtin/login/modules/code-login.vue b/src/views/_builtin/login/modules/code-login.vue index 8431aa7a..adfb517a 100644 --- a/src/views/_builtin/login/modules/code-login.vue +++ b/src/views/_builtin/login/modules/code-login.vue @@ -1,24 +1,58 @@