feat:框架-短信验证(之前没提交相关部分) 和 短信验证前加拦截校验码避免短信轰炸
This commit is contained in:
@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user