新增登录页图形验证码功能,支持通过Redis全局配置控制验证码的启用状态,优化登录流程以提升用户体验。新增相关API接口和前端配置项,确保验证码逻辑与后端同步。

This commit is contained in:
geht
2026-04-20 14:21:36 +08:00
parent 7a648b20be
commit 73426a7af3
23 changed files with 450 additions and 103 deletions

View File

@@ -8,25 +8,27 @@
<InputPassword size="large" visibilityToggle v-model:value="formData.password" :placeholder="t('sys.login.password')" />
</FormItem>
<!--验证码-->
<ARow class="enter-x">
<ACol :span="12">
<FormItem name="inputCode" class="enter-x">
<Input size="large" v-model:value="formData.inputCode" :placeholder="t('sys.login.inputCode')" style="min-width: 100px" />
</FormItem>
</ACol>
<ACol :span="8">
<FormItem :style="{ 'text-align': 'right', 'margin-left': '20px' }" class="enter-x">
<img
v-if="randCodeData.requestCodeSuccess"
style="margin-top: 2px; max-width: initial"
:src="randCodeData.randCodeImage"
@click="handleChangeCheckCode"
/>
<img v-else style="margin-top: 2px; max-width: initial" src="../../../assets/images/checkcode.png" @click="handleChangeCheckCode" />
</FormItem>
</ACol>
</ARow>
<!-- 无需验证码时下列整块输入框 + 图片均不挂载不请求 randomImage -->
<template v-if="captchaVisible">
<ARow class="enter-x">
<ACol :span="12">
<FormItem name="inputCode" class="enter-x">
<Input size="large" v-model:value="formData.inputCode" :placeholder="t('sys.login.inputCode')" style="min-width: 100px" />
</FormItem>
</ACol>
<ACol :span="8">
<FormItem :style="{ 'text-align': 'right', 'margin-left': '20px' }" class="enter-x">
<img
v-if="randCodeData.requestCodeSuccess"
style="margin-top: 2px; max-width: initial"
:src="randCodeData.randCodeImage"
@click="handleChangeCheckCode"
/>
<img v-else style="margin-top: 2px; max-width: initial" src="../../../assets/images/checkcode.png" @click="handleChangeCheckCode" />
</FormItem>
</ACol>
</ARow>
</template>
<ARow class="enter-x">
<ACol :span="12">
@@ -98,7 +100,7 @@
import { useUserStore } from '/@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
import { useDesign } from '/@/hooks/web/useDesign';
import { getCodeInfo } from '/@/api/sys/user';
import { getCodeInfo, getLoginCaptchaServerConfig } from '/@/api/sys/user';
import { encryptAESCBC } from '/@/utils/cipher';
const ACol = Col;
@@ -113,6 +115,26 @@
const { prefixCls } = useDesign('login');
const userStore = useUserStore();
/**
* 是否展示验证码(与后端是否校验图形码一致)。
* 浏览器无法直连 Redis调用 GET /sys/loginCaptchaConfig由后端读取 Redis 全局开关,无值时再回落 jeecg.firewall.enable-login-captcha。
* null 表示尚未拉取完成,不展示验证码区域,避免误请求 randomImage。
*/
const serverRequiresCaptcha = ref<boolean | null>(null);
const captchaVisible = computed(() => serverRequiresCaptcha.value === true);
/** 兼容接口 result 为 boolean / 字符串 */
function parseCaptchaRequired(raw: unknown): boolean {
if (raw === true || raw === 'true' || raw === 1 || raw === '1') {
return true;
}
if (raw === false || raw === 'false' || raw === 0 || raw === '0') {
return false;
}
return false;
}
const { setLoginState, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
@@ -132,6 +154,13 @@
checkKey: null,
});
function clearCaptchaUiState() {
formData.inputCode = '';
randCodeData.randCodeImage = '';
randCodeData.requestCodeSuccess = false;
randCodeData.checkKey = null;
}
const { validForm } = useFormValid(formRef);
//onKeyStroke('Enter', handleLogin);
@@ -146,15 +175,16 @@
// 密码使用AES加密传输
const encryptedPassword = encryptAESCBC(data.password);
const { userInfo } = await userStore.login(
toRaw({
password: encryptedPassword,
username: data.account,
captcha: data.inputCode,
checkKey: randCodeData.checkKey,
mode: 'none', //不要默认的错误提示
})
);
const loginPayload: Recordable = {
password: encryptedPassword,
username: data.account,
mode: 'none', //不要默认的错误提示
};
if (unref(captchaVisible)) {
loginPayload.captcha = data.inputCode;
loginPayload.checkKey = randCodeData.checkKey;
}
const { userInfo } = await userStore.login(toRaw(loginPayload));
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
@@ -169,7 +199,9 @@
duration: 3,
});
loading.value = false;
handleChangeCheckCode();
if (unref(captchaVisible)) {
handleChangeCheckCode();
}
}
}
function handleChangeCheckCode() {
@@ -189,8 +221,17 @@
function onThirdLogin(type) {
thirdModalRef.value.onThirdLogin(type);
}
//初始化验证码
onMounted(() => {
onMounted(async () => {
try {
const enabled = await getLoginCaptchaServerConfig();
serverRequiresCaptcha.value = parseCaptchaRequired(enabled);
} catch {
serverRequiresCaptcha.value = false;
}
if (!unref(captchaVisible)) {
clearCaptchaUiState();
return;
}
handleChangeCheckCode();
});
</script>