Files
geg-gas-web/src/views/secondDev/LoginForm.vue
2025-06-05 17:57:37 +08:00

482 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>