perf: 登录失败时清除验证码状态

v2
xiaojunnuo 2025-09-24 00:06:00 +08:00
parent 2c1600ddfb
commit 1c15beadc7
8 changed files with 130 additions and 58 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<component :is="captchaComponent" v-if="settingStore.inited" ref="captchaRef" class="captcha_input" :captcha-get="getCaptcha" @change="onChange" /> <component :is="captchaComponent" v-if="settingStore.inited" ref="captchaRef" :model-value="modelValue" class="captcha_input" :captcha-get="getCaptcha" @change="onChange" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, defineAsyncComponent } from "vue"; import { ref, computed, defineAsyncComponent } from "vue";
@ -7,6 +7,12 @@ import { useSettingStore } from "/@/store/settings";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { request } from "/@/api/service"; import { request } from "/@/api/service";
const props = defineProps({
modelValue: {
type: Object,
default: () => ({}),
},
});
const captchaRef = ref(null); const captchaRef = ref(null);
const settingStore = useSettingStore(); const settingStore = useSettingStore();
@ -17,7 +23,7 @@ const captchaAddonId = computed(() => {
return settingStore.sysPublic.captchaAddonId ?? 0; return settingStore.sysPublic.captchaAddonId ?? 0;
}); });
const captchaComponent = computed(() => { const captchaComponent = computed(() => {
let type = "image"; let type: any = "image";
if (settingStore.sysPublic.captchaAddonId && settingStore.sysPublic.captchaType) { if (settingStore.sysPublic.captchaAddonId && settingStore.sysPublic.captchaType) {
type = settingStore.sysPublic.captchaType; type = settingStore.sysPublic.captchaType;
} }
@ -36,7 +42,7 @@ async function getCaptcha(): Promise<any> {
}); });
} }
function onChange(data) { function onChange(data: any) {
emits("update:modelValue", data); emits("update:modelValue", data);
emits("change", data); emits("change", data);
} }
@ -44,7 +50,11 @@ function onChange(data) {
async function getCaptchaForm() { async function getCaptchaForm() {
return await captchaRef.value.getCaptchaForm(); return await captchaRef.value.getCaptchaForm();
} }
async function reset() {
await captchaRef.value.reset();
}
defineExpose({ defineExpose({
getCaptchaForm, getCaptchaForm,
reset,
}); });
</script> </script>

View File

@ -2,7 +2,7 @@
<div ref="captchaRef" class="geetest_captcha_wrapper"></div> <div ref="captchaRef" class="geetest_captcha_wrapper"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted } from "vue"; import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { request } from "/src/api/service"; import { request } from "/src/api/service";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
@ -12,13 +12,14 @@ defineOptions({
}); });
const emit = defineEmits(["update:modelValue", "change"]); const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{ const props = defineProps<{
modelValue: any;
captchaGet: () => Promise<any>; captchaGet: () => Promise<any>;
}>(); }>();
const captchaRef = ref(null); const captchaRef = ref(null);
// const addonApi = createAddonApi(); // const addonApi = createAddonApi();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const captchaInstanceRef = ref({}); const captchaInstanceRef: Ref = ref({});
async function init() { async function init() {
// if (!initGeetest4) { // if (!initGeetest4) {
// await import("https://static.geetest.com/v4/gt4.js"); // await import("https://static.geetest.com/v4/gt4.js");
@ -35,6 +36,13 @@ async function init() {
captcha.appendTo(captchaRef.value); // appendTo captcha.appendTo(captchaRef.value); // appendTo
captchaInstanceRef.value.instance = captcha; captchaInstanceRef.value.instance = captcha;
captchaInstanceRef.value.captchaId = captchaId; captchaInstanceRef.value.captchaId = captchaId;
captcha.onSuccess(function () {
const form = getCaptchaForm();
if (form) {
emitChange(form);
}
});
} }
); );
} }
@ -58,29 +66,51 @@ function getCaptchaForm() {
return result; return result;
} }
const valueRef = ref(null); // const valueRef = ref(null);
const timeoutId = setInterval(() => { // const timeoutId = setInterval(() => {
const form = getCaptchaForm(); // const form = getCaptchaForm();
if (form && valueRef.value != form) { // if (form && valueRef.value != form) {
console.log("form", form); // console.log("form", form);
valueRef.value = form; // valueRef.value = form;
emitChange(form); // emitChange(form);
} // }
}, 1000); // }, 1000);
onUnmounted(() => { // onUnmounted(() => {
clearTimeout(timeoutId); // clearTimeout(timeoutId);
}); // });
function emitChange(value: string) { function emitChange(value: string) {
emit("update:modelValue", value); emit("update:modelValue", value);
emit("change", value); emit("change", value);
} }
function reset() {
captchaInstanceRef.value.instance.reset();
}
watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
defineExpose({ defineExpose({
getCaptchaForm, getCaptchaForm,
reset,
}); });
watch(
() => [props.captchaGet],
async () => {
await init();
}
);
onMounted(async () => { onMounted(async () => {
await init(); await init();
}); });

View File

@ -11,10 +11,11 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineExpose, defineProps, ref } from "vue"; import { defineEmits, defineExpose, defineProps, ref, watch } from "vue";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
const props = defineProps<{ const props = defineProps<{
modelValue: any;
captchaGet?: () => Promise<any>; captchaGet?: () => Promise<any>;
}>(); }>();
defineOptions({ defineOptions({
@ -42,6 +43,7 @@ function getCaptchaForm() {
defineExpose({ defineExpose({
resetImageCode, resetImageCode,
getCaptchaForm, getCaptchaForm,
reset: resetImageCode,
}); });
resetImageCode(); resetImageCode();
@ -52,7 +54,18 @@ function onChange(value: string) {
emitChange(form); emitChange(form);
} }
function emitChange(value) { watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
function emitChange(value: any) {
emit("update:modelValue", value); emit("update:modelValue", value);
emit("change", value); emit("change", value);
} }

View File

@ -21,10 +21,17 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item has-feedback name="captchaForEmail" label="验证码"> <a-form-item has-feedback name="captchaForEmail" label="验证码">
<CaptchaInput v-model:model-value="formState.captchaForEmail"></CaptchaInput> <CaptchaInput ref="captchaForEmailRef" v-model:model-value="formState.captchaForEmail"></CaptchaInput>
</a-form-item> </a-form-item>
<a-form-item has-feedback name="validateCode" label="邮件验证码"> <a-form-item has-feedback name="validateCode" label="邮件验证码">
<email-code v-model:value="formState.validateCode" :captcha="formState.captchaForEmail" :email="formState.input" :random-str="formState.randomStr" verification-type="forgotPassword" /> <email-code
v-model:value="formState.validateCode"
:captcha="formState.captchaForEmail"
:email="formState.input"
:random-str="formState.randomStr"
verification-type="forgotPassword"
@error="formState.captchaForEmail = null"
/>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="mobile" tab="手机号找回"> <a-tab-pane key="mobile" tab="手机号找回">
@ -36,10 +43,17 @@
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item has-feedback name="captchaForSms" label="验证码"> <a-form-item has-feedback name="captchaForSms" label="验证码">
<CaptchaInput v-model:model-value="formState.captchaForSms"></CaptchaInput> <CaptchaInput ref="captchaForSmsRef" v-model:model-value="formState.captchaForSms"></CaptchaInput>
</a-form-item> </a-form-item>
<a-form-item name="validateCode" label="手机验证码"> <a-form-item name="validateCode" label="手机验证码">
<sms-code v-model:value="formState.validateCode" :captcha="formState.captchaForSms" :mobile="formState.input" :phone-code="formState.phoneCode" verification-type="forgotPassword" /> <sms-code
v-model:value="formState.validateCode"
:captcha="formState.captchaForSms"
:mobile="formState.input"
:phone-code="formState.phoneCode"
verification-type="forgotPassword"
@error="formState.captchaForSms = null"
/>
</a-form-item> </a-form-item>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
@ -141,15 +155,20 @@ watch(forgotPasswordType, () => {
}); });
const handleFinish = async (values: any) => { const handleFinish = async (values: any) => {
await userStore.forgotPassword( try {
toRaw({ await userStore.forgotPassword(
type: forgotPasswordType.value, toRaw({
input: formState.input, type: forgotPasswordType.value,
validateCode: formState.validateCode, input: formState.input,
password: formState.password, validateCode: formState.validateCode,
confirmPassword: formState.confirmPassword, password: formState.password,
}) as any confirmPassword: formState.confirmPassword,
); }) as any
);
} finally {
formState.captchaForSms = null;
formState.captchaForEmail = null;
}
}; };
const handleFinishFailed = (errors: any) => { const handleFinishFailed = (errors: any) => {

View File

@ -41,7 +41,7 @@
</a-form-item> </a-form-item>
<a-form-item name="smsCode" :rules="rules.smsCode"> <a-form-item name="smsCode" :rules="rules.smsCode">
<sms-code v-model:value="formState.smsCode" :captcha="formState.smsCaptcha" :mobile="formState.mobile" :phone-code="formState.phoneCode" /> <sms-code v-model:value="formState.smsCode" :captcha="formState.smsCaptcha" :mobile="formState.mobile" :phone-code="formState.phoneCode" @error="formState.smsCaptcha = null" />
</a-form-item> </a-form-item>
</template> </template>
</a-tab-pane> </a-tab-pane>
@ -188,6 +188,8 @@ export default defineComponent({
} }
} finally { } finally {
loading.value = false; loading.value = false;
formState.captcha = null;
formState.smsCaptcha = null;
} }
}; };
@ -209,18 +211,6 @@ export default defineComponent({
const captchaInputRef = ref(); const captchaInputRef = ref();
const captchaInputForSmsCode = ref(); const captchaInputForSmsCode = ref();
async function doCaptchaValidate() {
if (!sysPublicSettings.captchaEnabled) {
return {};
}
const res = await captchaInputRef.value.getValidatedForm();
if (!res) {
return false;
}
return {
...res,
};
}
return { return {
t, t,

View File

@ -24,7 +24,7 @@ const props = defineProps<{
captcha?: any; captcha?: any;
verificationType?: string; verificationType?: string;
}>(); }>();
const emit = defineEmits(["update:value", "change"]); const emit = defineEmits(["update:value", "change", "error"]);
function onChange(value: string) { function onChange(value: string) {
emit("update:value", value); emit("update:value", value);
@ -59,6 +59,9 @@ async function sendSmsCode() {
captcha: props.captcha, captcha: props.captcha,
verificationType: props.verificationType, verificationType: props.verificationType,
}); });
} catch (e) {
emit("error", e);
throw e;
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -23,7 +23,7 @@ const props = defineProps<{
captcha?: any; captcha?: any;
verificationType?: string; verificationType?: string;
}>(); }>();
const emit = defineEmits(["update:value", "change"]); const emit = defineEmits(["update:value", "change", "error"]);
function onChange(value: string) { function onChange(value: string) {
emit("update:value", value); emit("update:value", value);
@ -54,6 +54,9 @@ async function sendSmsCode() {
captcha: props.captcha, captcha: props.captcha,
verificationType: props.verificationType, verificationType: props.verificationType,
}); });
} catch (e) {
emit("error", e);
throw e;
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -66,7 +66,7 @@
</a-form-item> </a-form-item>
<a-form-item has-feedback name="validateCode" :rules="rules.validateCode" label="邮件验证码"> <a-form-item has-feedback name="validateCode" :rules="rules.validateCode" label="邮件验证码">
<email-code v-model:value="formState.validateCode" :captcha="formState.captchaForEmail" :email="formState.email" /> <email-code v-model:value="formState.validateCode" :captcha="formState.captchaForEmail" :email="formState.email" @error="formState.captchaForEmail = null" />
</a-form-item> </a-form-item>
</template> </template>
</a-tab-pane> </a-tab-pane>
@ -182,16 +182,20 @@ export default defineComponent({
}; };
const handleFinish = async (values: any) => { const handleFinish = async (values: any) => {
await userStore.register( try {
toRaw({ await userStore.register(
type: registerType.value, toRaw({
password: formState.password, type: registerType.value,
username: formState.username, password: formState.password,
email: formState.email, username: formState.username,
captcha: formState.captcha, email: formState.email,
validateCode: formState.validateCode, captcha: formState.captcha,
}) as any validateCode: formState.validateCode,
); }) as any
);
} finally {
formState.captcha = null;
}
}; };
const handleFinishFailed = (errors: any) => { const handleFinishFailed = (errors: any) => {