mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 15:53:43 +08:00 
			
		
		
		
	feat(projects): login page: code-login
This commit is contained in:
		@@ -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';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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: {
 | 
			
		||||
        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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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: '请输入确认密码',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -367,6 +367,8 @@ declare namespace App {
 | 
			
		||||
          codeLogin: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            getCode: string;
 | 
			
		||||
            reGetCode: string;
 | 
			
		||||
            sendCodeSuccess: string;
 | 
			
		||||
            imageCodePlaceholder: string;
 | 
			
		||||
          };
 | 
			
		||||
          register: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,58 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, reactive } from 'vue';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
 | 
			
		||||
import { useCaptcha } from '@/hooks/business/captcha';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'CodeLogin'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NForm size="large" :show-label="false">
 | 
			
		||||
    <NFormItem>
 | 
			
		||||
      <NInput :placeholder="$t('page.login.common.phonePlaceholder')" />
 | 
			
		||||
  <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
 | 
			
		||||
    <NFormItem path="phone">
 | 
			
		||||
      <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
 | 
			
		||||
    </NFormItem>
 | 
			
		||||
    <NFormItem>
 | 
			
		||||
      <NInput :placeholder="$t('page.login.common.codePlaceholder')" />
 | 
			
		||||
    <NFormItem path="code">
 | 
			
		||||
      <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>
 | 
			
		||||
    <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') }}
 | 
			
		||||
      </NButton>
 | 
			
		||||
      <NButton size="large" round block @click="toggleLoginModule('pwd-login')">
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ async function handleSubmit() {
 | 
			
		||||
      <NInput
 | 
			
		||||
        v-model:value="model.password"
 | 
			
		||||
        type="password"
 | 
			
		||||
        show-password-on="mousedown"
 | 
			
		||||
        show-password-on="click"
 | 
			
		||||
        :placeholder="$t('page.login.common.passwordPlaceholder')"
 | 
			
		||||
      />
 | 
			
		||||
    </NFormItem>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user