UNIVPLMDataIntegration/Web/src/views/login/component/account.vue
zuohuaijun 128d774310 😎清理代码
2025-03-24 03:19:07 +08:00

490 lines
14 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>
<el-tooltip :visible="state.capsLockVisible" effect="light" :content="t('message.account.lockTag')" placement="top">
<el-form ref="ruleFormRef" :model="state.ruleForm" size="large" :rules="state.rules" class="login-content-form">
<el-form-item class="login-animation1" prop="account">
<el-input ref="accountRef" text :placeholder="t('message.account.accountneed')" v-model="state.ruleForm.account" clearable autocomplete="off" @keyup.enter.native="handleSignIn">
<template #prefix>
<el-icon>
<ele-User />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation2" prop="password">
<el-input
ref="passwordRef"
:type="state.isShowPassword ? 'text' : 'password'"
:placeholder="t('message.account.passwordneed')"
v-model="state.ruleForm.password"
autocomplete="off"
@keyup.enter.native="handleSignIn"
>
<template #prefix>
<el-icon>
<ele-Unlock />
</el-icon>
</template>
<template #suffix>
<i class="iconfont el-input__icon login-content-password" :class="state.isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'" @click="state.isShowPassword = !state.isShowPassword">
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation3" prop="captcha" v-if="state.captchaEnabled">
<el-col :span="15">
<el-input
ref="codeRef"
text
maxlength="4"
:placeholder="$t('message.account.accountPlaceholder3')"
v-model="state.ruleForm.code"
clearable
autocomplete="off"
@keyup.enter.native="handleSignIn"
>
<template #prefix>
<el-icon>
<ele-Position />
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="8">
<div :class="[state.expirySeconds > 0 ? 'login-content-code' : 'login-content-code-expired']" @click="getCaptcha">
<div style="width: 130px; height: 38px; text-align: center; cursor: pointer" v-if="state.captchaImage == ''">
<el-icon class="is-loading">
<ele-Loading />
</el-icon>
</div>
<img class="login-content-code-img" width="130px" height="38px" :src="state.captchaImage" style="cursor: pointer" v-else />
</div>
</el-col>
</el-form-item>
<el-form-item class="login-animation4">
<el-button type="primary" icon="ele-Promotion" class="login-content-submit" round v-waves @click="handleSignIn" :loading="state.loading.signIn">
<span>{{ $t('message.account.accountBtnText') }}</span>
</el-button>
</el-form-item>
<div class="font12 mt30 login-animation4 login-msg">{{ $t('message.mobile.msgText') }}</div>
<div class="change-language">
<div class="change-language-title">{{ $t('message.account.changeLanguage') }}:</div>
<div style="cursor: pointer">
<el-dropdown size="small" :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange" placement="top-end">
<div class="layout-navbars-breadcrumb-user-icon" style="margin-left: 5px">
<FlagIcon :code="currentCountryCode" :size="18" :title="$t('message.user.title1')" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(value, key) in languageList" :key="key" :command="key" :disabled="state.disabledI18n === key">
<div class="flex items-center">
<div class="mr-2">
<FlagIcon :code="getCountryCode(key)" :size="18" />
</div>
<div style="margin-left: 10px">
{{ value }}
</div>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- <el-button type="primary" round v-waves @click="weixinSignIn" :loading="state.loading.signIn"></el-button> -->
</el-form>
</el-tooltip>
<div class="dialog-header">
<el-dialog v-model="state.rotateVerifyVisible" :show-close="false">
<DragVerifyImgRotate
ref="dragRef"
:imgsrc="state.rotateVerifyImg"
v-model:isPassing="state.isPassRotate"
:text="t('message.account.splitslive')"
:successText="t('message.account.success')"
handlerIcon="fa fa-angle-double-right"
successIcon="fa fa-hand-peace-o"
@passcallback="passRotateVerify"
/>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="loginAccount">
import { reactive, computed, ref, onMounted, defineAsyncComponent, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, InputInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import { Local, Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import { sm2 } from 'sm-crypto-v2';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { storeToRefs } from 'pinia';
import { accessTokenKey, clearTokens, feature, getAPI } from '/@/utils/axios-utils';
import { SysAuthApi } from '/@/api-services/api';
import { languageList, getCountryCode } from '/@/i18n';
import FlagIcon from 'vue3-flag-icons';
import type { CountryCode } from 'vue3-flag-icons';
// 旋转图片滑块组件
// import verifyImg from '/@/assets/logo-mini.svg';
const DragVerifyImgRotate = defineAsyncComponent(() => import('/@/components/dragVerify/dragVerifyImgRotate.vue'));
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { t, locale } = useI18n();
const route = useRoute();
const router = useRouter();
const ruleFormRef = ref();
const accountRef = ref<InputInstance>();
const passwordRef = ref<InputInstance>();
const codeRef = ref<InputInstance>();
const accountNeed = t('message.account.accountneed');
const passwordNeed = t('message.account.passwordneed');
const codeNeed = t('message.account.codeNeed');
const loginFail = t('message.account.loginfail');
const notPrivilege = t('message.account.notprivilege');
const dragRef: any = ref(null);
const currentCountryCode = ref<CountryCode>(getCountryCode(themeConfig.value.globalI18n));
const state = reactive({
isShowPassword: false,
ruleForm: {
account: window.__env__.VITE_DEFAULT_USER,
password: window.__env__.VITE_DEFAULT_USER_PASSWORD,
code: '',
codeId: 0 as any,
},
rules: {
account: [{ required: true, message: accountNeed, trigger: 'blur' }],
password: [{ required: true, message: passwordNeed, trigger: 'blur' }],
code: [{ required: true, message: codeNeed, trigger: 'blur' }],
},
loading: {
signIn: false,
},
captchaImage: '',
rotateVerifyVisible: false,
// rotateVerifyImg: verifyImg,
rotateVerifyImg: themeConfig.value.logoUrl,
secondVerEnabled: false,
captchaEnabled: false,
isPassRotate: false,
capsLockVisible: false,
expirySeconds: 60 as any, // 验证码过期时间
disabledI18n: 'zh-CN',
});
const onLanguageChange = (lang: string) => {
Local.remove('themeConfig');
themeConfig.value.globalI18n = lang;
Local.set('themeConfig', themeConfig.value);
currentCountryCode.value = getCountryCode(lang);
locale.value = lang;
initI18nOrSize('globalI18n', 'disabledI18n');
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
(<any>state)[attr] = Local.get('themeConfig')[value];
};
// 验证码过期计时器
let timer: any = null;
// 页面初始化
onMounted(async () => {
// 若URL带有Token参数第三方登录
const accessToken = route.query.token;
if (accessToken) await saveTokenAndInitRoutes(accessToken);
// // 若公钥为空则刷新页面
// if (window.__env__.VITE_SM_PUBLIC_KEY == '' || window.__env__.VITE_SM_PUBLIC_KEY == undefined) {
// window.location.reload();
// }
// 获取登录配置
state.secondVerEnabled = themeConfig.value.secondVer ?? false;
state.captchaEnabled = themeConfig.value.captcha ?? true;
// 获取验证码
getCaptcha();
// 注册验证码过期计时器
if (state.captchaEnabled) {
timer = setInterval(() => {
if (state.expirySeconds > 0) state.expirySeconds -= 1;
}, 1000);
}
// 检测大小写按键/CapsLK
document.addEventListener('keyup', handleKeyPress);
});
// 页面卸载
onUnmounted(() => {
// 销毁验证码过期计时器
clearInterval(timer);
timer = null;
document.removeEventListener('keyup', handleKeyPress);
});
// 检测大小写按键
const handleKeyPress = (e: KeyboardEvent) => {
state.capsLockVisible = e.key === 'CapsLock';
};
// 获取验证码
const getCaptcha = async () => {
if (!state.captchaEnabled) return;
state.ruleForm.code = '';
const res = await getAPI(SysAuthApi)
.apiSysAuthCaptchaGet()
.then((res) => res.data.result);
state.captchaImage = 'data:text/html;base64,' + res?.img;
state.expirySeconds = res?.expirySeconds;
state.ruleForm.codeId = res?.id;
};
// 获取时间
const currentTime = computed(() => {
return formatAxis(new Date(), t);
});
// 登录
const onSignIn = async () => {
ruleFormRef.value.validate(async (valid: boolean) => {
if (!valid) return false;
try {
state.loading.signIn = true;
// SM2加密密码
// const keys = SM2.generateKeyPair();
const publicKey = window.__env__.VITE_SM_PUBLIC_KEY;
const password = sm2.doEncrypt(state.ruleForm.password, publicKey, 1);
const tenantid = route.query.tid ?? 0;
const [err, res] = await feature(getAPI(SysAuthApi).apiSysAuthLoginPost({ ...state.ruleForm, password: password, tenantId: Number(tenantid) }));
if (err) {
getCaptcha(); // 重新获取验证码
return;
}
if (res.data.result?.accessToken == undefined) {
getCaptcha(); // 重新获取验证码
ElMessage.error(loginFail);
return;
}
await saveTokenAndInitRoutes(res.data.result?.accessToken);
} finally {
state.loading.signIn = false;
}
});
};
// 保持Token并初始化路由
const saveTokenAndInitRoutes = async (accessToken: string | any) => {
// 缓存token
Local.set(accessTokenKey, accessToken);
// Local.set(refreshAccessTokenKey, refreshAccessToken);
Session.set('token', accessToken);
// 添加完动态路由再进行router跳转否则可能报错 No match found for location with path "/"
const isNoPower = await initBackEndControlRoutes();
signInSuccess(isNoPower); // 再执行 signInSuccess
};
// 登录成功后的跳转
const signInSuccess = (isNoPower: boolean | undefined) => {
if (isNoPower) {
ElMessage.warning(notPrivilege);
clearTokens(); // 清空Token缓存
} else {
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
const stores = useRoutesList();
const { routesList } = storeToRefs(stores);
const recursion = (routeList: any[], url: string): boolean | undefined => {
if (routeList && routeList.length > 0) {
for (let i = 0; i < routeList.length; i++) {
if (routeList[i].path === url) return true;
if (routeList[i]?.children.length > 0) {
let result = recursion(routeList[i]?.children, url);
if (result) return true;
}
}
}
};
let exist = recursion(routesList.value, route.query?.redirect as string);
if (exist) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else router.push('/');
} else {
router.push('/');
}
// 登录成功提示
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
}
};
// 打开旋转验证
const openRotateVerify = () => {
state.rotateVerifyVisible = true;
state.isPassRotate = false;
dragRef.value?.reset();
};
// 通过旋转验证
const passRotateVerify = () => {
state.rotateVerifyVisible = false;
state.isPassRotate = true;
onSignIn();
};
// 登录处理
const handleSignIn = () => {
if (!state.ruleForm.account) {
accountRef.value?.focus();
} else if (!state.ruleForm.password) {
passwordRef.value?.focus();
} else if (state.captchaEnabled && !state.ruleForm.code) {
codeRef.value?.focus();
} else {
state.secondVerEnabled ? openRotateVerify() : onSignIn();
}
};
// // 微信登录
// const weixinSignIn = () => {
// window.open('http://localhost:5005/api/sysoauth/signin?provider=Gitee&redirectUrl=http://localhost:8888');
// };
// 导出对象
defineExpose({ saveTokenAndInitRoutes });
</script>
<style lang="scss" scoped>
.dialog-header {
:deep(.el-dialog) {
width: unset !important;
.el-dialog__header {
display: none;
}
.el-dialog__wrapper {
position: absolute !important;
}
.v-modal {
position: absolute !important;
}
}
}
.login-content-form {
margin-top: 20px;
@for $i from 0 through 4 {
.login-animation#{$i} {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: calc($i/10) + s;
}
}
.login-content-password {
display: inline-block;
width: 20px;
cursor: pointer;
&:hover {
color: #909399;
}
}
.login-content-code {
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
.login-content-code-img {
width: 100%;
height: 40px;
line-height: 40px;
background-color: #ffffff;
border: 1px solid rgb(220, 223, 230);
cursor: pointer;
transition: all ease 0.2s;
border-radius: 4px;
user-select: none;
&:hover {
border-color: #c0c4cc;
transition: all ease 0.2s;
}
}
}
.login-content-code-expired {
@extend .login-content-code;
&::before {
content: t('message.account.captchaExpired');
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
color: #ffffff;
text-align: center;
}
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
margin-top: 15px;
}
.login-msg {
color: var(--el-text-color-placeholder);
}
.change-language {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 5px;
margin-right: 20px;
.change-language-title {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
</style>