You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

502 lines
14 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="login">
<div class="login-brand-panel">
<div class="brand-mask"></div>
<div class="brand-content">
<div class="brand-logo">
<svg-icon icon-class="dashboard" class="logo-icon" />
<span class="logo-title">{{ logoTitle }}</span>
</div>
<h1 class="brand-title">{{ title }}</h1>
<p class="brand-subtitle">仓库控制系统WCS</p>
<ul class="brand-features">
<li>仓库 / 库位 / 物料主数据统一管理</li>
<li>输送线AGV提升机等设备统一调度</li>
<li>实时任务队列与路径执行可视化</li>
<li>多租户隔离安全可靠的权限体系</li>
</ul>
</div>
</div>
<div class="login-form-wrapper">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-box">
<h3 class="title">{{ title }}</h3>
<lang-select />
</div>
<el-form-item v-if="tenantEnabled" prop="tenantId">
<el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.password')"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaEnabled" prop="code">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.code')"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<el-form-item style="float: right">
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
<svg-icon icon-class="wechat" />
</el-button>
<el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">
<svg-icon icon-class="maxkey" />
</el-button>
<el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">
<svg-icon icon-class="topiam" />
</el-button>
<el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">
<svg-icon icon-class="gitee" />
</el-button>
<el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">
<svg-icon icon-class="github" />
</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
<span v-else>{{ proxy.$t('login.logging') }}</span>
</el-button>
<div v-if="register" style="float: right">
<router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
</div>
</el-form-item>
</el-form>
</div>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © {{ currentYear }} {{ logoTitle }} All Rights Reserved.</span>
</div>
</div>
</template>
<script setup lang="ts">
import { getCodeImg, getTenantList } from '@/api/login';
import { authRouterUrl } from '@/api/system/social/auth';
import { useUserStore } from '@/store/modules/user';
import { LoginData, TenantVO } from '@/api/types';
import { to } from 'await-to-js';
import { HttpStatus } from '@/enums/RespEnum';
import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const title = import.meta.env.VITE_APP_TITLE;
const logoTitle = import.meta.env.VITE_APP_LOGO_TITLE;
const currentYear = new Date().getFullYear();
const userStore = useUserStore();
const router = useRouter();
const { t } = useI18n();
const loginForm = ref<LoginData>({
tenantId: '000000',
username: 'admin',
password: 'admin123',
rememberMe: false,
code: '',
uuid: ''
} as LoginData);
const loginRules: ElFormRules = {
tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
};
const codeUrl = ref('');
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true);
// 租户开关
const tenantEnabled = ref(true);
// 注册开关
const register = ref(false);
const redirect = ref('/');
const loginRef = ref<ElFormInstance>();
// 租户列表
const tenantList = ref<TenantVO[]>([]);
watch(
() => router.currentRoute.value,
(newRoute: any) => {
redirect.value = newRoute.query && newRoute.query.redirect && decodeURIComponent(newRoute.query.redirect);
},
{ immediate: true }
);
const handleLogin = () => {
loginRef.value?.validate(async (valid: boolean, fields: any) => {
if (valid) {
loading.value = true;
// 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
localStorage.setItem('tenantId', String(loginForm.value.tenantId));
localStorage.setItem('username', String(loginForm.value.username));
localStorage.setItem('password', String(loginForm.value.password));
localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
} else {
// 否则移除
localStorage.removeItem('tenantId');
localStorage.removeItem('username');
localStorage.removeItem('password');
localStorage.removeItem('rememberMe');
}
// 调用action的登录方法
const [err] = await to(userStore.login(loginForm.value));
if (!err) {
const redirectUrl = redirect.value || '/';
await router.push(redirectUrl);
loading.value = false;
} else {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
await getCode();
}
}
} else {
console.log('error submit!', fields);
}
});
};
/**
* 获取验证码
*/
const getCode = async () => {
const res = await getCodeImg();
const { data } = res;
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
if (captchaEnabled.value) {
// 刷新验证码时清空输入框
loginForm.value.code = '';
codeUrl.value = 'data:image/gif;base64,' + data.img;
loginForm.value.uuid = data.uuid;
}
};
const getLoginData = () => {
const tenantId = localStorage.getItem('tenantId');
const username = localStorage.getItem('username');
const password = localStorage.getItem('password');
const rememberMe = localStorage.getItem('rememberMe');
loginForm.value = {
tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
username: username === null ? String(loginForm.value.username) : username,
password: password === null ? String(loginForm.value.password) : String(password),
rememberMe: rememberMe === null ? false : Boolean(rememberMe)
} as LoginData;
};
/**
* 获取租户列表
*/
const initTenantList = async () => {
const { data } = await getTenantList(false);
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (tenantEnabled.value) {
tenantList.value = data.voList;
if (tenantList.value != null && tenantList.value.length !== 0) {
loginForm.value.tenantId = tenantList.value[0].tenantId;
}
}
};
/**
* 第三方登录
* @param type
*/
const doSocialLogin = (type: string) => {
authRouterUrl(type, loginForm.value.tenantId).then((res: any) => {
if (res.code === HttpStatus.SUCCESS) {
// 获取授权地址跳转
window.location.href = res.data;
} else {
ElMessage.error(res.msg);
}
});
};
onMounted(() => {
getCode();
initTenantList();
getLoginData();
});
</script>
<style lang="scss" scoped>
.login {
display: flex;
height: 100%;
min-height: 100vh;
background-image: url('../assets/images/login-background.jpg');
background-size: cover;
background-position: center;
position: relative;
}
.login-brand-panel {
display: none;
flex: 1;
position: relative;
color: #ffffff;
padding: 60px;
align-items: center;
overflow: hidden;
.brand-mask {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.88), rgba(30, 58, 138, 0.78));
}
.brand-content {
position: relative;
z-index: 1;
max-width: 560px;
}
.brand-logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
.logo-icon {
width: 42px;
height: 42px;
color: #60a5fa;
}
.logo-title {
font-size: 22px;
font-weight: 700;
letter-spacing: 1px;
}
}
.brand-title {
font-size: 38px;
font-weight: 700;
margin: 0 0 12px 0;
line-height: 1.2;
}
.brand-subtitle {
font-size: 20px;
font-weight: 400;
margin: 0 0 40px 0;
color: rgba(255, 255, 255, 0.82);
letter-spacing: 2px;
}
.brand-features {
list-style: none;
padding: 0;
margin: 0;
font-size: 16px;
line-height: 2.2;
color: rgba(255, 255, 255, 0.78);
li {
position: relative;
padding-left: 24px;
&::before {
content: '';
position: absolute;
left: 0;
top: 14px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #60a5fa;
}
}
}
@media (min-width: 992px) {
display: flex;
}
}
.login-form-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 24px;
z-index: 1;
@media (min-width: 992px) {
width: 520px;
min-width: 520px;
}
}
.title-box {
display: flex;
align-items: center;
gap: 8px;
.title {
margin: 0px auto 26px auto;
text-align: center;
color: var(--el-text-color-primary);
font-weight: 600;
letter-spacing: 0.5px;
}
:deep(.lang-select--style) {
line-height: 0;
color: var(--el-text-color-secondary);
}
}
.login-form {
border-radius: var(--app-radius-lg);
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(255, 255, 255, 0.5);
width: min(420px, 100%);
padding: 32px 30px 12px 30px;
z-index: 1;
box-shadow: var(--app-shadow-lg);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
.el-input {
height: 40px;
input {
height: 40px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 0px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-form :deep(.el-input__wrapper) {
background-color: rgba(255, 255, 255, 0.9);
}
.login-form :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.login-form :deep(.el-button--primary) {
border-radius: var(--app-radius-md);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
}
.login-form :deep(.el-button.is-circle) {
background: rgba(15, 23, 42, 0.04);
border: 1px solid rgba(15, 23, 42, 0.08);
color: var(--el-text-color-regular);
}
.login-form :deep(.el-button.is-circle:hover) {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.2);
}
.login-code {
width: calc(37% - 10px);
height: 40px;
float: right;
margin-left: 10px;
box-sizing: border-box;
border-radius: var(--app-radius-sm);
overflow: hidden;
background: rgba(255, 255, 255, 0.9);
border: 1px solid var(--el-border-color-light);
img {
cursor: pointer;
vertical-align: middle;
display: block;
width: 100%;
height: 40px;
object-fit: cover;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.75);
font-family: Arial, serif;
font-size: 12px;
letter-spacing: 1px;
z-index: 2;
}
.login-code-img {
height: 40px;
padding-left: 0;
}
:global(html.dark) {
.login-form {
background: rgba(17, 24, 39, 0.9);
border-color: rgba(148, 163, 184, 0.2);
}
.login-form :deep(.el-input__wrapper) {
background-color: rgba(17, 24, 39, 0.7);
}
.login-form :deep(.el-button.is-circle) {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.25);
color: #e5e7eb;
}
.el-login-footer {
color: rgba(226, 232, 240, 0.65);
}
}
</style>