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 @@
-
-
-
+
+
+
-
-
+
+
+
+
+ {{ label }}
+
+
-
+
{{ $t('common.confirm') }}
diff --git a/src/views/_builtin/login/modules/pwd-login.vue b/src/views/_builtin/login/modules/pwd-login.vue
index 1998f9ec..252112ce 100644
--- a/src/views/_builtin/login/modules/pwd-login.vue
+++ b/src/views/_builtin/login/modules/pwd-login.vue
@@ -49,7 +49,7 @@ async function handleSubmit() {