新增登录页图形验证码功能,支持通过Redis全局配置控制验证码的启用状态,优化登录流程以提升用户体验。新增相关API接口和前端配置项,确保验证码逻辑与后端同步。
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user