feat(projects): login page: code-login
This commit is contained in:
parent
f91ef30bd5
commit
c91dd282a6
@ -1,9 +1,10 @@
|
|||||||
import useBoolean from './use-boolean';
|
import useBoolean from './use-boolean';
|
||||||
import useLoading from './use-loading';
|
import useLoading from './use-loading';
|
||||||
|
import useCountDown from './use-count-down';
|
||||||
import useContext from './use-context';
|
import useContext from './use-context';
|
||||||
import useSvgIconRender from './use-svg-icon-render';
|
import useSvgIconRender from './use-svg-icon-render';
|
||||||
import useHookTable from './use-table';
|
import useHookTable from './use-table';
|
||||||
|
|
||||||
export { useBoolean, useLoading, useContext, useSvgIconRender, useHookTable };
|
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
||||||
|
|
||||||
export * from './use-table';
|
export * from './use-table';
|
||||||
|
49
packages/hooks/src/use-count-down.ts
Normal file
49
packages/hooks/src/use-count-down.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
|
}
|
71
src/hooks/business/captcha.ts
Normal file
71
src/hooks/business/captcha.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
|
}
|
@ -184,6 +184,8 @@ const local: App.I18n.Schema = {
|
|||||||
codeLogin: {
|
codeLogin: {
|
||||||
title: 'Verification Code Login',
|
title: 'Verification Code Login',
|
||||||
getCode: 'Get verification code',
|
getCode: 'Get verification code',
|
||||||
|
reGetCode: 'Reacquire after {time}s',
|
||||||
|
sendCodeSuccess: 'Verification code sent successfully',
|
||||||
imageCodePlaceholder: 'Please enter image verification code'
|
imageCodePlaceholder: 'Please enter image verification code'
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
@ -391,7 +393,7 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
pwd: {
|
pwd: {
|
||||||
required: 'Please enter password',
|
required: 'Please enter password',
|
||||||
invalid: 'Password format is incorrect'
|
invalid: '6-18 characters, including letters, numbers, and underscores'
|
||||||
},
|
},
|
||||||
confirmPwd: {
|
confirmPwd: {
|
||||||
required: 'Please enter password again',
|
required: 'Please enter password again',
|
||||||
|
@ -184,6 +184,8 @@ const local: App.I18n.Schema = {
|
|||||||
codeLogin: {
|
codeLogin: {
|
||||||
title: '验证码登录',
|
title: '验证码登录',
|
||||||
getCode: '获取验证码',
|
getCode: '获取验证码',
|
||||||
|
reGetCode: '{time}秒后重新获取',
|
||||||
|
sendCodeSuccess: '验证码发送成功',
|
||||||
imageCodePlaceholder: '请输入图片验证码'
|
imageCodePlaceholder: '请输入图片验证码'
|
||||||
},
|
},
|
||||||
register: {
|
register: {
|
||||||
@ -391,7 +393,7 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
pwd: {
|
pwd: {
|
||||||
required: '请输入密码',
|
required: '请输入密码',
|
||||||
invalid: '密码格式不正确'
|
invalid: '密码格式不正确,6-18位字符,包含字母、数字、下划线'
|
||||||
},
|
},
|
||||||
confirmPwd: {
|
confirmPwd: {
|
||||||
required: '请输入确认密码',
|
required: '请输入确认密码',
|
||||||
|
2
src/typings/app.d.ts
vendored
2
src/typings/app.d.ts
vendored
@ -367,6 +367,8 @@ declare namespace App {
|
|||||||
codeLogin: {
|
codeLogin: {
|
||||||
title: string;
|
title: string;
|
||||||
getCode: string;
|
getCode: string;
|
||||||
|
reGetCode: string;
|
||||||
|
sendCodeSuccess: string;
|
||||||
imageCodePlaceholder: string;
|
imageCodePlaceholder: string;
|
||||||
};
|
};
|
||||||
register: {
|
register: {
|
||||||
|
@ -1,24 +1,58 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive } from 'vue';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
|
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||||
|
import { useCaptcha } from '@/hooks/business/captcha';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'CodeLogin'
|
name: 'CodeLogin'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { toggleLoginModule } = useRouterPush();
|
const { toggleLoginModule } = useRouterPush();
|
||||||
|
const { formRef, validate } = useNaiveForm();
|
||||||
|
const { label, isCounting, loading, getCaptcha } = useCaptcha();
|
||||||
|
|
||||||
|
interface FormModel {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model: FormModel = reactive({
|
||||||
|
phone: '',
|
||||||
|
code: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||||
|
const { formRules } = useFormRules();
|
||||||
|
|
||||||
|
return {
|
||||||
|
phone: formRules.phone,
|
||||||
|
code: formRules.code
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await validate();
|
||||||
|
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NForm size="large" :show-label="false">
|
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||||
<NFormItem>
|
<NFormItem path="phone">
|
||||||
<NInput :placeholder="$t('page.login.common.phonePlaceholder')" />
|
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem>
|
<NFormItem path="code">
|
||||||
<NInput :placeholder="$t('page.login.common.codePlaceholder')" />
|
<div class="w-full flex-y-center gap-16px">
|
||||||
|
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||||
|
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
|
||||||
|
{{ label }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NSpace vertical :size="18" class="w-full">
|
<NSpace vertical :size="18" class="w-full">
|
||||||
<NButton type="primary" size="large" round block>
|
<NButton type="primary" size="large" round block @click="handleSubmit">
|
||||||
{{ $t('common.confirm') }}
|
{{ $t('common.confirm') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
||||||
|
@ -49,7 +49,7 @@ async function handleSubmit() {
|
|||||||
<NInput
|
<NInput
|
||||||
v-model:value="model.password"
|
v-model:value="model.password"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="mousedown"
|
show-password-on="click"
|
||||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
Loading…
Reference in New Issue
Block a user