482 lines
17 KiB
Vue
482 lines
17 KiB
Vue
<template>
|
||
<Form v-show="getShow" ref="formRef" :model="formData" :rules="getFormRules" class="p-4 enter-x form-box" @keypress.enter="handleLogin">
|
||
<div>
|
||
<FormItem class="enter-x" name="account" v-if="loginType =='pw'">
|
||
<label class="form-title"> {{ t('账号') }}</label>
|
||
<Input v-model:value="formData.account" :placeholder="t('账号')" class="fix-auto-fill" size="large" style="height: 58px;" @blur="handleBlur">
|
||
<template #prefix>
|
||
<IconFontSymbol class="user-icon" icon="yonghu-xianxing" />
|
||
</template>
|
||
</Input>
|
||
</FormItem>
|
||
<FormItem class="enter-x" name="password" v-if="loginType =='pw'">
|
||
<label class="form-title"> {{ t('密码') }}</label>
|
||
<InputPassword v-model:value="formData.password" :placeholder="t('密码')" size="large" style="height: 58px;" visibilityToggle>
|
||
<template #prefix>
|
||
<IconFontSymbol class="user-icon" icon="mima" />
|
||
</template>
|
||
</InputPassword>
|
||
</FormItem>
|
||
<FormItem class="enter-x" name="captchaCode" v-if="loginType =='pw' && loginUseType == 'captcha'">
|
||
<label class="form-title"> {{ t('图形验证码') }}</label>
|
||
<Input v-model:value="formData.captchaCode" :placeholder="t('图形验证码')" class="fix-auto-fill" size="large" style="height: 58px;" visibilityToggle>
|
||
<template #prefix>
|
||
<PictureOutlined :style="{fontSize: '25px', color: '#717c91'}"/>
|
||
</template>
|
||
<template #suffix>
|
||
<a-image
|
||
:src="captchaImage.imgBase64 || ''"
|
||
alt="验证码"
|
||
class="captcha-image"
|
||
/>
|
||
<ReloadOutlined :style="{fontSize: '25px', color: '#717c91'}" @click="refreshCaptcha"/>
|
||
</template>
|
||
</Input>
|
||
</FormItem>
|
||
|
||
<FormItem v-if="getAppEnvConfig().VITE_GLOB_TENANT_ENABLED && loginType =='pw'" name="tenantCode" class="enter-x">
|
||
<label class="form-title"> {{ t('租户码') }}</label>
|
||
<Input
|
||
size="large"
|
||
visibilityToggle
|
||
v-model:value="formData.tenantCode"
|
||
:placeholder="t('租户码')"
|
||
style="height: 58px; width: 450px;"
|
||
>
|
||
<template #prefix>
|
||
<IconFontSymbol icon="zuzhiguanli" class="user-icon" />
|
||
</template>
|
||
</Input>
|
||
</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 #prefix>
|
||
<PhoneOutlined :style="{fontSize: '25px', color: '#717c91'}"/>
|
||
</template>
|
||
<template #suffix>
|
||
<Button type="link" class="f-16" @click="getLoginCode" size="small" :disabled="codeButtonDisabled">
|
||
{{ getCodeButtonName }}
|
||
</Button>
|
||
</template>
|
||
</Input>
|
||
</FormItem>
|
||
<ARow class="enter-x">
|
||
<ACol :span="12">
|
||
<FormItem v-if="loginType =='pw'">
|
||
<!-- No logic, you need to deal with it yourself -->
|
||
<Checkbox v-model:checked="rememberMe" class="f-16" size="small">
|
||
{{ t('记住我') }}
|
||
</Checkbox>
|
||
</FormItem>
|
||
</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">
|
||
<FormItem :style="{ 'text-align': 'right' }">
|
||
No logic, you need to deal with it yourself
|
||
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
|
||
{{ t('忘记密码?') }}
|
||
</Button>
|
||
</FormItem>
|
||
</ACol> -->
|
||
</ARow>
|
||
|
||
<FormItem class="enter-x">
|
||
<Button :loading="loading" :style="{ 'border-radius': '8px' }" block class="sub-button" type="primary" @click="handleLogin">
|
||
{{ t('登录') }}
|
||
</Button>
|
||
</FormItem>
|
||
</div>
|
||
</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>
|
||
<script lang="ts" setup>
|
||
import { reactive, ref, unref, computed, onMounted } from 'vue';
|
||
import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue';
|
||
|
||
import { useI18n } from '/@/hooks/web/useI18n';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
|
||
import { useUserStore } from '/@/store/modules/user';
|
||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from '/@/views/sys/login/useLogin';
|
||
import { getMobileLoginCode, getMobileLoginImg, sendMobileLoginCode, checkAccountCaptchaApi, } from '/@/api/system/login';
|
||
import { useDesign } from '/@/hooks/web/useDesign';
|
||
import { Base64 } from 'js-base64';
|
||
|
||
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { getAppEnvConfig } from '/@/utils/env';
|
||
import Icon from '/@/components/Icon/index';
|
||
|
||
import { PhoneOutlined, PictureOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||
|
||
const ACol = Col;
|
||
const ARow = Row;
|
||
const FormItem = Form.Item;
|
||
const InputPassword = Input.Password;
|
||
const { t } = useI18n();
|
||
const { notification, createErrorModal } = useMessage();
|
||
const { prefixCls } = useDesign('login');
|
||
const userStore = useUserStore();
|
||
|
||
const { currentRoute } = useRouter();
|
||
|
||
const { getLoginState, setLoginState } = useLoginState();
|
||
const { getFormRules } = useFormRules();
|
||
|
||
const formRef = ref();
|
||
// const iframeRef = ref();
|
||
const loading = ref(false);
|
||
const rememberMe = ref(false);
|
||
const loginType = ref('mobile')
|
||
const loginUseType = ref();
|
||
const countdown = ref(60)
|
||
|
||
const visible = ref(false);
|
||
const refreshLoading = ref(false);
|
||
const imgObj = ref({
|
||
imgBase64: ''
|
||
})
|
||
const captchaImage = ref({
|
||
imgBase64: ''
|
||
})
|
||
const imgCode = ref('')
|
||
|
||
const formData = reactive({
|
||
account: '',
|
||
password: '',
|
||
mobile: '',
|
||
code: '',
|
||
tenantCode: 'system',
|
||
captchaCode: ''
|
||
});
|
||
const getCodeButtonName = ref('获取验证码')
|
||
const codeButtonDisabled = ref(false)
|
||
let setCodeInterval = null
|
||
|
||
onMounted(async () => {
|
||
//如果是第三方登录跳转回来 会携带token
|
||
if (currentRoute.value.query.token) {
|
||
oauthLogin(currentRoute.value.query.token)
|
||
}
|
||
//如果第三方登录 登录错误 会携带错误信息
|
||
if (currentRoute.value.query.error) {
|
||
createErrorModal({
|
||
title: t('错误提示'),
|
||
content: t(currentRoute.value.query.error as string),
|
||
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
|
||
});
|
||
}
|
||
const loginInfo = window.localStorage.getItem('USER__LOGIN__INFO__');
|
||
if (loginInfo) {
|
||
formData.account = Base64.decode(JSON.parse(loginInfo).account);
|
||
formData.password = Base64.decode(JSON.parse(loginInfo).password);
|
||
formData.tenantCode = Base64.decode(JSON.parse(loginInfo).tenantCode);
|
||
}
|
||
});
|
||
const oauthLogin = async (token) => {
|
||
try {
|
||
loading.value = true;
|
||
const userInfo = await userStore.oauthLogin({
|
||
token: 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;
|
||
}
|
||
}
|
||
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
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 账号失焦 请求后台获取业务状态
|
||
*/
|
||
async function handleBlur() {
|
||
if (!formData.account) {
|
||
return;
|
||
}
|
||
let checkAccountCaptcha = await checkAccountCaptchaApi({username: formData.account})
|
||
if(checkAccountCaptcha == true) {
|
||
loginUseType.value = 'captcha';
|
||
// setLoginState(LoginStateEnum.LOGIN_WITH_CAPTCHA)
|
||
refreshCaptcha();
|
||
} else {
|
||
loginUseType.value = '';
|
||
// setLoginState(LoginStateEnum.LOGIN)
|
||
}
|
||
}
|
||
|
||
function refreshTodo() {
|
||
refreshLoading.value = true
|
||
onMobileLoginImg()
|
||
refreshLoading.value = false
|
||
}
|
||
|
||
// 图形验证
|
||
async function onMobileLoginImg() {
|
||
imgObj.value = await getMobileLoginImg({account: formData.mobile})
|
||
}
|
||
|
||
// 图形账号验证
|
||
async function refreshCaptcha() {
|
||
captchaImage.value = await getMobileLoginImg({account: formData.account})
|
||
}
|
||
|
||
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 {
|
||
// 校验有没带图形验证码提交
|
||
if (loginUseType.value == 'captcha' && !data.captchaCode) {
|
||
createErrorModal({
|
||
title: t('错误提示'),
|
||
content: '请输入图形验证码',
|
||
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
|
||
});
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
const userInfo = await userStore.login({
|
||
password: data.password,
|
||
userName: data.account,
|
||
tenantCode: data.tenantCode,
|
||
captchaCode: data.captchaCode,
|
||
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) {
|
||
if ((error as unknown as Error).message.includes('验证码')) {
|
||
loginUseType.value = 'captcha';
|
||
// setLoginState(LoginStateEnum.LOGIN_WITH_CAPTCHA);
|
||
refreshCaptcha();
|
||
}
|
||
createErrorModal({
|
||
title: t('错误提示'),
|
||
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
|
||
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
|
||
});
|
||
} finally {
|
||
loading.value = false;
|
||
handleBlur();
|
||
}
|
||
} 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>
|
||
<style lang="less" scoped>
|
||
.form-box {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.f-16 {
|
||
font-size: 16px;
|
||
}
|
||
|
||
:deep(.ant-checkbox-inner) {
|
||
border-color: #ced5f2;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 0;
|
||
}
|
||
|
||
:deep(.ant-form-item input[type='checkbox']) {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
|
||
.form-title {
|
||
line-height: 40px;
|
||
display: block;
|
||
}
|
||
|
||
.sub-button {
|
||
height: 48px;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.ant-form label {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.user-icon {
|
||
font-size: 26px;
|
||
margin-right: 8px;
|
||
color: #707c92;
|
||
}
|
||
|
||
.ant-input-affix-wrapper {
|
||
border-color: #ced5f2;
|
||
border-style: none none solid;
|
||
}
|
||
|
||
:deep(.ant-input-password-icon) {
|
||
font-size: 20px;
|
||
color: #707c92;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
:deep(.ant-input-affix-wrapper-lg) {
|
||
padding-left: 0;
|
||
}
|
||
|
||
.login-modal-content {
|
||
text-align: center;
|
||
|
||
.refresh {
|
||
text-align: right;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
:deep(.captcha-image) {
|
||
cursor: pointer;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
height: 48px;
|
||
}
|
||
// :deep(.phone-outlined-class) {
|
||
// height: 25.99px;
|
||
// width: 25.99px;
|
||
// }
|
||
</style>
|