475 lines
11 KiB
Vue
475 lines
11 KiB
Vue
<template>
|
|
<div :class="prefixCls" class="login-container">
|
|
<div class="login-content">
|
|
<!-- 登录卡片 -->
|
|
<div class="login-card">
|
|
<!-- Logo 图标 -->
|
|
<div class="login-logo">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 1024 1024" width="60" height="60">
|
|
<path
|
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372S306.6 140 512 140s372 166.6 372 372-166.6 372-372 372z"
|
|
fill="#1677ff"
|
|
/>
|
|
<path
|
|
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
|
|
fill="#1677ff"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 标题 -->
|
|
<h2 class="login-title">智慧小区云平台</h2>
|
|
|
|
<!-- 登录表单 -->
|
|
<el-form
|
|
ref="formLogin"
|
|
:model="loginData.loginForm"
|
|
:rules="LoginRules"
|
|
class="login-form"
|
|
size="large"
|
|
>
|
|
<el-form-item prop="username">
|
|
<el-input
|
|
v-model="loginData.loginForm.username"
|
|
placeholder="请输入用户名称"
|
|
:prefix-icon="iconAvatar"
|
|
@blur="handleUsernameBlur"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item prop="password">
|
|
<el-input
|
|
v-model="loginData.loginForm.password"
|
|
placeholder="请输入登录密码"
|
|
:prefix-icon="iconLock"
|
|
show-password
|
|
type="password"
|
|
@keyup.enter="getCode()"
|
|
@blur="handlePasswordBlur"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<!-- 租户选择 -->
|
|
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantId">
|
|
<el-select
|
|
ref="tenantSelectRef"
|
|
v-model="loginData.loginForm.tenantId"
|
|
placeholder="请选择租户"
|
|
:prefix-icon="iconHouse"
|
|
style="width: 100%"
|
|
:disabled="tenantList.length === 0"
|
|
>
|
|
<el-option
|
|
v-for="tenant in tenantList"
|
|
:key="tenant.tenantId"
|
|
:label="tenant.tenantName"
|
|
:value="tenant.tenantId"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<!-- 记住密码 -->
|
|
<div class="login-options">
|
|
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
|
记住密码
|
|
</el-checkbox>
|
|
</div>
|
|
|
|
<!-- 登录按钮 -->
|
|
<el-form-item>
|
|
<el-button
|
|
:loading="loginLoading"
|
|
type="primary"
|
|
class="login-button"
|
|
@click="getCode()"
|
|
>
|
|
登录
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<!-- 右侧插图 -->
|
|
<div class="login-illustration">
|
|
<img src="@/assets/svgs/login-box-bg.svg" alt="illustration" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主题和语言切换 -->
|
|
<div class="login-tools">
|
|
<ThemeSwitch />
|
|
<LocaleDropdown />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { underlineToHump } from '@/utils'
|
|
import { useDesign } from '@/hooks/web/useDesign'
|
|
import { useAppStore } from '@/store/modules/app'
|
|
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
|
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
|
import { useIcon } from '@/hooks/web/useIcon'
|
|
import * as authUtil from '@/utils/auth'
|
|
import { usePermissionStore } from '@/store/modules/permission'
|
|
import * as LoginApi from '@/api/login'
|
|
import { ElLoading } from 'element-plus'
|
|
|
|
defineOptions({ name: 'Login' })
|
|
|
|
const { t } = useI18n()
|
|
const message = useMessage()
|
|
const appStore = useAppStore()
|
|
const { getPrefixCls } = useDesign()
|
|
const prefixCls = getPrefixCls('login')
|
|
|
|
const iconHouse = useIcon({ icon: 'ep:house' })
|
|
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
|
const iconLock = useIcon({ icon: 'ep:lock' })
|
|
|
|
const formLogin = ref()
|
|
const loginLoading = ref(false)
|
|
const captchaType = ref('blockPuzzle')
|
|
const tenantList = ref<LoginApi.TenantInfo[]>([])
|
|
const tenantSelectRef = ref()
|
|
const isFetchingTenants = ref(false)
|
|
|
|
const LoginRules = {
|
|
username: [required],
|
|
password: [required],
|
|
tenantId: [
|
|
{
|
|
required: true,
|
|
message: '请选择租户',
|
|
trigger: 'change'
|
|
}
|
|
]
|
|
}
|
|
|
|
const loginData = reactive({
|
|
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
|
loginForm: {
|
|
tenantId: undefined as number | undefined,
|
|
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
|
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
|
captchaVerification: '',
|
|
rememberMe: true
|
|
}
|
|
})
|
|
|
|
const verify = ref()
|
|
const loading = ref()
|
|
const { push } = useRouter()
|
|
const permissionStore = usePermissionStore()
|
|
|
|
// 用户名失焦时获取租户列表
|
|
const handleUsernameBlur = async () => {
|
|
if (loginData.tenantEnable !== 'true') {
|
|
return
|
|
}
|
|
await fetchTenantList()
|
|
}
|
|
|
|
// 密码失焦时获取租户列表
|
|
const handlePasswordBlur = async () => {
|
|
if (loginData.tenantEnable !== 'true') {
|
|
return
|
|
}
|
|
await fetchTenantList()
|
|
}
|
|
|
|
// 获取租户列表
|
|
const fetchTenantList = async () => {
|
|
const username = loginData.loginForm.username?.trim()
|
|
const password = loginData.loginForm.password?.trim()
|
|
|
|
if (!username || !password) {
|
|
return
|
|
}
|
|
|
|
if (isFetchingTenants.value) {
|
|
return
|
|
}
|
|
|
|
isFetchingTenants.value = true
|
|
|
|
try {
|
|
const res = await LoginApi.getUserTenantList({
|
|
username,
|
|
password
|
|
})
|
|
|
|
if (!res || !res.tenants || res.tenants.length === 0) {
|
|
message.error('无权限访问该系统')
|
|
tenantList.value = []
|
|
loginData.loginForm.tenantId = undefined
|
|
return
|
|
}
|
|
|
|
tenantList.value = res.tenants
|
|
|
|
if (res.tenants.length === 1) {
|
|
loginData.loginForm.tenantId = res.tenants[0].tenantId
|
|
} else {
|
|
loginData.loginForm.tenantId = undefined
|
|
}
|
|
} catch (error) {
|
|
console.error('获取租户列表失败:', error)
|
|
tenantList.value = []
|
|
loginData.loginForm.tenantId = undefined
|
|
} finally {
|
|
isFetchingTenants.value = false
|
|
}
|
|
}
|
|
|
|
// 获取验证码
|
|
const getCode = async () => {
|
|
if (loginData.tenantEnable === 'true' && tenantList.value.length === 0) {
|
|
const username = loginData.loginForm.username?.trim()
|
|
const password = loginData.loginForm.password?.trim()
|
|
|
|
if (!username || !password) {
|
|
message.error('请输入用户名和密码')
|
|
return
|
|
}
|
|
|
|
await fetchTenantList()
|
|
|
|
if (tenantList.value.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (tenantList.value.length === 1) {
|
|
loginData.loginForm.tenantId = tenantList.value[0].tenantId
|
|
continueLogin()
|
|
} else {
|
|
if (tenantSelectRef.value) {
|
|
tenantSelectRef.value.visible = true
|
|
}
|
|
message.info('请选择要登录的租户')
|
|
}
|
|
} else if (loginData.tenantEnable === 'true' && tenantList.value.length > 0) {
|
|
if (!loginData.loginForm.tenantId) {
|
|
message.error('请选择租户')
|
|
return
|
|
}
|
|
continueLogin()
|
|
} else {
|
|
continueLogin()
|
|
}
|
|
}
|
|
|
|
const continueLogin = () => {
|
|
if (loginData.captchaEnable === 'false') {
|
|
handleLogin({})
|
|
} else {
|
|
verify.value?.show()
|
|
}
|
|
}
|
|
|
|
const setTenantId = () => {
|
|
if (loginData.loginForm.tenantId) {
|
|
authUtil.setTenantId(loginData.loginForm.tenantId)
|
|
}
|
|
}
|
|
|
|
const getLoginFormCache = () => {
|
|
const loginForm = authUtil.getLoginForm()
|
|
if (loginForm) {
|
|
loginData.loginForm = {
|
|
...loginData.loginForm,
|
|
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
|
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
|
rememberMe: loginForm.rememberMe
|
|
}
|
|
}
|
|
}
|
|
|
|
const getTenantByWebsite = async () => {
|
|
if (loginData.tenantEnable === 'true') {
|
|
const website = location.host
|
|
const res = await LoginApi.getTenantByWebsite(website)
|
|
if (res) {
|
|
authUtil.setTenantId(res.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleLogin = async (params: any) => {
|
|
loginLoading.value = true
|
|
try {
|
|
setTenantId()
|
|
await formLogin.value.validate()
|
|
const loginDataLoginForm = { ...loginData.loginForm }
|
|
loginDataLoginForm.captchaVerification = params.captchaVerification
|
|
const res = await LoginApi.login(loginDataLoginForm)
|
|
if (!res) {
|
|
return
|
|
}
|
|
loading.value = ElLoading.service({
|
|
lock: true,
|
|
text: '正在加载系统中...',
|
|
background: 'rgba(0, 0, 0, 0.7)'
|
|
})
|
|
if (loginDataLoginForm.rememberMe) {
|
|
authUtil.setLoginForm(loginDataLoginForm)
|
|
} else {
|
|
authUtil.removeLoginForm()
|
|
}
|
|
authUtil.setToken(res)
|
|
await push({ path: '/' })
|
|
} finally {
|
|
loginLoading.value = false
|
|
loading.value?.close()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
getLoginFormCache()
|
|
getTenantByWebsite()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.login-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100vh;
|
|
background: #1677ff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.login-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 100px;
|
|
max-width: 1200px;
|
|
width: 100%;
|
|
padding: 0 40px;
|
|
}
|
|
|
|
.login-card {
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
padding: 40px;
|
|
width: 380px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.login-logo {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 16px;
|
|
|
|
.logo-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.login-title {
|
|
text-align: center;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #1677ff;
|
|
margin: 0 0 30px 0;
|
|
}
|
|
|
|
.login-form {
|
|
:deep(.el-input__wrapper) {
|
|
border-radius: 4px;
|
|
}
|
|
|
|
:deep(.el-select .el-input__wrapper) {
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
|
|
.login-options {
|
|
margin-bottom: 20px;
|
|
|
|
:deep(.el-checkbox__label) {
|
|
color: #1677ff;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
.login-button {
|
|
width: 100%;
|
|
height: 44px;
|
|
font-size: 16px;
|
|
background: #1677ff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
|
|
&:hover {
|
|
background: #4096ff;
|
|
}
|
|
}
|
|
|
|
.login-illustration {
|
|
flex-shrink: 0;
|
|
|
|
img {
|
|
width: 500px;
|
|
height: auto;
|
|
}
|
|
}
|
|
|
|
.login-tools {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
|
|
:deep(svg) {
|
|
fill: #ffffff;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.login-illustration {
|
|
display: none;
|
|
}
|
|
|
|
.login-content {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.login-card {
|
|
width: 100%;
|
|
padding: 30px 20px;
|
|
}
|
|
|
|
.login-content {
|
|
padding: 0 20px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.dark .login-form {
|
|
.el-divider__text {
|
|
background-color: var(--login-bg-color);
|
|
}
|
|
|
|
.el-card {
|
|
background-color: var(--login-bg-color);
|
|
}
|
|
}
|
|
</style>
|