feat:框架-短信验证(之前没提交相关部分) 和 短信验证前加拦截校验码避免短信轰炸

This commit is contained in:
lvjunzhao
2025-02-13 17:51:27 +08:00
parent b631009738
commit 3372b6854c
2 changed files with 241 additions and 54 deletions

View File

@ -32,6 +32,8 @@ enum Api {
logoInfo = '/system/logoConfig/logo-info', logoInfo = '/system/logoConfig/logo-info',
loginInfo = '/system/loginConfig/info', loginInfo = '/system/loginConfig/info',
loginConfig = '/system/loginConfig', loginConfig = '/system/loginConfig',
mobileLoginCode = '/system/captcha',
mobileLoginImg = '/system/captchaImg',
} }
/** /**
@ -233,3 +235,30 @@ export function setLoginConfig(params, mode: ErrorMessageMode = 'modal') {
}, },
); );
} }
export function getMobileLoginImg(params, mode: ErrorMessageMode = 'modal') {
return defHttp.get(
{
url: Api.mobileLoginImg,
params: params
},
{
errorMessageMode: mode,
},
);
}
export function getMobileLoginCode(params, mode: ErrorMessageMode = 'modal') {
return defHttp.get(
{ url: Api.mobileLoginCode, params: params },
{
errorMessageMode: mode,
},
);
}
export function sendMobileLoginCode(params, mode: ErrorMessageMode = 'modal') {
return defHttp.post(
{ url: Api.mobileLoginCode, data: params },
{
errorMessageMode: mode,
},
);
}

View File

@ -1,46 +1,73 @@
<template> <template>
<Form v-show="getShow" ref="formRef" :model="formData" :rules="getFormRules" class="p-4 enter-x form-box" @keypress.enter="handleLogin"> <Form v-show="getShow" ref="formRef" :model="formData" :rules="getFormRules" class="p-4 enter-x form-box" @keypress.enter="handleLogin">
<div> <div>
<FormItem class="enter-x" name="account"> <FormItem class="enter-x" name="account" v-if="loginType =='pw'">
<label class="form-title"> {{ t('账号') }}</label> <label class="form-title"> {{ t('账号') }}</label>
<Input v-model:value="formData.account" :placeholder="t('账号')" class="fix-auto-fill" size="large" style="height: 58px"> <Input v-model:value="formData.account" :placeholder="t('账号')" class="fix-auto-fill" size="large" style="height: 58px;">
<template #prefix> <template #prefix>
<IconFontSymbol class="user-icon" icon="yonghu-xianxing" /> <IconFontSymbol class="user-icon" icon="yonghu-xianxing" />
</template> </template>
</Input> </Input>
</FormItem> </FormItem>
<FormItem class="enter-x" name="password"> <FormItem class="enter-x" name="password" v-if="loginType =='pw'">
<label class="form-title"> {{ t('密码') }}</label> <label class="form-title"> {{ t('密码') }}</label>
<InputPassword v-model:value="formData.password" :placeholder="t('密码')" size="large" style="height: 58px" visibilityToggle> <InputPassword v-model:value="formData.password" :placeholder="t('密码')" size="large" style="height: 58px;" visibilityToggle>
<template #prefix> <template #prefix>
<IconFontSymbol class="user-icon" icon="mima" /> <IconFontSymbol class="user-icon" icon="mima" />
</template> </template>
</InputPassword> </InputPassword>
</FormItem> </FormItem>
<FormItem v-if="getAppEnvConfig().VITE_TENANT_ENABLED" name="tenantCode" class="enter-x"> <FormItem v-if="getAppEnvConfig().VITE_TENANT_ENABLED && loginType =='pw'" name="tenantCode" class="enter-x">
<label class="form-title"> {{ t('租户码') }}</label> <label class="form-title"> {{ t('租户码') }}</label>
<Input <Input
size="large" size="large"
visibilityToggle visibilityToggle
v-model:value="formData.tenantCode" v-model:value="formData.tenantCode"
:placeholder="t('租户码')" :placeholder="t('租户码')"
style="height: 58px" style="height: 58px; width: 450px;"
> >
<template #prefix> <template #prefix>
<IconFontSymbol icon="zuzhiguanli" class="user-icon" /> <IconFontSymbol icon="zuzhiguanli" class="user-icon" />
</template> </template>
</Input> </Input>
</FormItem> </FormItem>
<FormItem class="enter-x" name="mobile" v-if="loginType =='mobile'">
<label class="form-title"> {{ t('手机号') }}</label>
<Input v-model:value="formData.mobile" :placeholder="t('手机号')" class="fix-auto-fill" size="large" style="height: 58px;">
<template #prefix>
<IconFontSymbol class="user-icon" icon="yonghu-xianxing" />
</template>
</Input>
</FormItem>
<FormItem class="enter-x" name="code" v-if="loginType =='mobile'">
<label class="form-title"> {{ t('验证码') }}</label>
<Input v-model:value="formData.code" :placeholder="t('验证码')" class="fix-auto-fill" size="large" style="height: 58px;">
<template #suffix>
<!-- <span>111</span> -->
<Button type="link" class="f-16" @click="getLoginCode" size="small" :disabled="codeButtonDisabled">
{{ getCodeButtonName }}
</Button>
</template>
</Input>
</FormItem>
<ARow class="enter-x"> <ARow class="enter-x">
<ACol :span="12"> <ACol :span="12">
<FormItem> <FormItem v-if="loginType =='pw'">
<!-- No logic, you need to deal with it yourself --> <!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" class="f-16" size="small"> <Checkbox v-model:checked="rememberMe" class="f-16" size="small">
{{ t('记住我') }} {{ t('记住我') }}
</Checkbox> </Checkbox>
</FormItem> </FormItem>
</ACol> </ACol>
<ACol :span="12" style="text-align: right;">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Button type="link" class="f-16" @click="changeLoginType" size="small">
{{ loginType == 'mobile' ? t('账号密码登录') : t('验证码登录') }}
</Button>
</FormItem>
</ACol>
<!-- <ACol :span="12"> <!-- <ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }"> <FormItem :style="{ 'text-align': 'right' }">
No logic, you need to deal with it yourself No logic, you need to deal with it yourself
@ -58,6 +85,29 @@
</FormItem> </FormItem>
</div> </div>
</Form> </Form>
<a-modal
v-model:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
:maskClosable="false"
centered
title="请完成下列验证后继续"
width="20%"
cancelText=""
>
<div class="login-modal-content">
<div class="refresh" @click="refreshTodo">
刷新
<Icon :spin="refreshLoading" icon="ant-design:redo-outlined" class="redo-outlined" />
</div>
<a-image
:width="200"
:src="imgObj.imgBase64 || ''"
/>
<Input v-model:value="imgCode" :placeholder="t('验证码')" size="large" />
</div>
</a-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, unref, computed, onMounted } from 'vue'; import { reactive, ref, unref, computed, onMounted } from 'vue';
@ -68,12 +118,14 @@
import { useUserStore } from '/@/store/modules/user'; import { useUserStore } from '/@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from '/@/views/sys/login/useLogin'; import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from '/@/views/sys/login/useLogin';
import { getMobileLoginCode, getMobileLoginImg, sendMobileLoginCode } from '/@/api/system/login';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue'; import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getAppEnvConfig } from '/@/utils/env'; import { getAppEnvConfig } from '/@/utils/env';
import Icon from '/@/components/Icon/index';
const ACol = Col; const ACol = Col;
const ARow = Row; const ARow = Row;
@ -93,38 +145,31 @@
// const iframeRef = ref(); // const iframeRef = ref();
const loading = ref(false); const loading = ref(false);
const rememberMe = ref(false); const rememberMe = ref(false);
const loginType = ref('mobile')
const countdown = ref(60)
const visible = ref(false);
const refreshLoading = ref(false);
const imgObj = ref({
imgBase64: ''
})
const imgCode = ref('')
const formData = reactive({ const formData = reactive({
account: '', account: '',
password: '', password: '',
mobile: '',
code: '',
tenantCode: 'system' tenantCode: 'system'
}); });
const getCodeButtonName = ref('获取验证码')
const codeButtonDisabled = ref(false)
let setCodeInterval = null
onMounted(async () => { onMounted(async () => {
//如果是第三方登录跳转回来 会携带token //如果是第三方登录跳转回来 会携带token
if (currentRoute.value.query.token) { if (currentRoute.value.query.token) {
try { oauthLogin(currentRoute.value.query.token)
loading.value = true;
const userInfo = await userStore.oauthLogin({
token: currentRoute.value.query.token as string,
mode: 'none' //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3
});
}
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
}
} }
//如果第三方登录 登录错误 会携带错误信息 //如果第三方登录 登录错误 会携带错误信息
if (currentRoute.value.query.error) { if (currentRoute.value.query.error) {
@ -141,23 +186,11 @@
formData.tenantCode = Base64.decode(JSON.parse(loginInfo).tenantCode); formData.tenantCode = Base64.decode(JSON.parse(loginInfo).tenantCode);
} }
}); });
const oauthLogin = async (token) => {
const { validForm } = useFormValid(formRef);
//onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN);
async function handleLogin() {
const data = await validForm();
if (!data) return;
try { try {
loading.value = true; loading.value = true;
const userInfo = await userStore.login({ const userInfo = await userStore.oauthLogin({
password: data.password, token: token as string,
userName: data.account,
tenantCode: data.tenantCode,
deviceType: 0, //pc-0,app-1
mode: 'none' //不要默认的错误提示 mode: 'none' //不要默认的错误提示
}); });
if (userInfo) { if (userInfo) {
@ -166,16 +199,6 @@
description: `${t('欢迎回来')}: ${userInfo.name}`, description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3 duration: 3
}); });
if (rememberMe.value) {
const info = {
account: Base64.encode(data.account),
password: Base64.encode(data.password),
tenantCode: Base64.encode(data.tenantCode)
};
window.localStorage.setItem('USER__LOGIN__INFO__', JSON.stringify(info));
} else {
window.localStorage.removeItem('USER__LOGIN__INFO__');
}
} }
} catch (error) { } catch (error) {
createErrorModal({ createErrorModal({
@ -187,6 +210,133 @@
loading.value = false; loading.value = false;
} }
} }
const changeLoginType = () => {
loginType.value = loginType.value == 'pw' ? 'mobile' : 'pw'
}
const getLoginCode = async () => {
countdown.value = 60
if(isValidPhoneNumber(formData.mobile)) {
visible.value = true
imgCode.value = ''
onMobileLoginImg()
} else {
notification.error({
message: t('手机号有误'),
description: `${t('手机号有误,请重新填写')}`,
duration: 3
});
}
}
function refreshTodo() {
refreshLoading.value = true
onMobileLoginImg()
refreshLoading.value = false
}
// 图形验证
async function onMobileLoginImg() {
imgObj.value = await getMobileLoginImg({mobile: formData.mobile})
}
async function handleOk() {
await getMobileLoginCode({captchaCode: imgCode.value ,mobile: formData.mobile})
setCodeInterval = setInterval(updateCountdown, 1000);
onVisible()
}
function handleCancel() {
onVisible()
}
function onVisible () {
visible.value = false
srcImg.value = ''
}
// 更新倒计时显示
function updateCountdown() {
if (countdown.value === 0) {
getCodeButtonName.value = '获取验证码';
codeButtonDisabled.value = false;
clearInterval(setCodeInterval)
} else {
countdown.value--;
getCodeButtonName.value = countdown.value + ' 秒后可重发';
codeButtonDisabled.value = true;
}
}
const isValidPhoneNumber = (phoneNumber) => {
// 中国大陆手机号码正则表达式
const reg = /^1[3|4|5|7|8]\d{9}$/
return reg.test(phoneNumber);
}
const { validForm } = useFormValid(formRef);
//onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN);
async function handleLogin() {
const data = await validForm();
if (!data) return;
if (loginType.value == 'pw') {
try {
loading.value = true;
const userInfo = await userStore.login({
password: data.password,
userName: data.account,
tenantCode: data.tenantCode,
deviceType: 0, //pc-0,app-1
mode: 'none' //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3
});
if (rememberMe.value) {
const info = {
account: Base64.encode(data.account),
password: Base64.encode(data.password),
tenantCode: Base64.encode(data.tenantCode)
};
window.localStorage.setItem('USER__LOGIN__INFO__', JSON.stringify(info));
} else {
window.localStorage.removeItem('USER__LOGIN__INFO__');
}
}
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
}
} else {
try {
let params = {
mobile: data.mobile,
code: data.code
}
let res = await sendMobileLoginCode(params)
await oauthLogin(res.token)
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
}
}
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.form-box { .form-box {
@ -211,6 +361,7 @@
.form-title { .form-title {
line-height: 40px; line-height: 40px;
display: block;
} }
.sub-button { .sub-button {
@ -242,4 +393,11 @@
:deep(.ant-input-affix-wrapper-lg) { :deep(.ant-input-affix-wrapper-lg) {
padding-left: 0; padding-left: 0;
} }
.login-modal-content {
text-align: center;
.refresh {
text-align: right;
cursor: pointer;
}
}
</style> </style>