|
| 1 | +<template> |
| 2 | + <div |
| 3 | + :class="prefixCls" |
| 4 | + class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px" |
| 5 | + > |
| 6 | + <div class="relative mx-auto h-full flex"> |
| 7 | + <div |
| 8 | + :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" |
| 9 | + > |
| 10 | + <!-- 左上角的 logo + 系统标题 --> |
| 11 | + <div class="relative flex items-center text-white"> |
| 12 | + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> |
| 13 | + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
| 14 | + </div> |
| 15 | + <!-- 左边的背景图 + 欢迎语 --> |
| 16 | + <div class="h-[calc(100%-60px)] flex items-center justify-center"> |
| 17 | + <TransitionGroup |
| 18 | + appear |
| 19 | + enter-active-class="animate__animated animate__bounceInLeft" |
| 20 | + tag="div" |
| 21 | + > |
| 22 | + <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> |
| 23 | + <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> |
| 24 | + <div key="3" class="mt-5 text-14px font-normal text-white"> |
| 25 | + {{ t('login.message') }} |
| 26 | + </div> |
| 27 | + </TransitionGroup> |
| 28 | + </div> |
| 29 | + </div> |
| 30 | + <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px"> |
| 31 | + <!-- 右上角的主题、语言选择 --> |
| 32 | + <div |
| 33 | + class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end" |
| 34 | + > |
| 35 | + <div class="flex items-center at-2xl:hidden at-xl:hidden"> |
| 36 | + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> |
| 37 | + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
| 38 | + </div> |
| 39 | + <div class="flex items-center justify-end space-x-10px"> |
| 40 | + <ThemeSwitch /> |
| 41 | + <LocaleDropdown class="dark:text-white lt-xl:text-white" /> |
| 42 | + </div> |
| 43 | + </div> |
| 44 | + <!-- 右边的登录界面 --> |
| 45 | + <Transition appear enter-active-class="animate__animated animate__bounceInRight"> |
| 46 | + <div |
| 47 | + class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px" |
| 48 | + > |
| 49 | + <!-- 账号登录 --> |
| 50 | + <el-form |
| 51 | + v-show="getShow" |
| 52 | + ref="formLogin" |
| 53 | + :model="loginData.loginForm" |
| 54 | + :rules="LoginRules" |
| 55 | + class="login-form" |
| 56 | + label-position="top" |
| 57 | + label-width="120px" |
| 58 | + size="large" |
| 59 | + > |
| 60 | + <el-row style="margin-right: -10px; margin-left: -10px"> |
| 61 | + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
| 62 | + <el-form-item> |
| 63 | + <LoginFormTitle style="width: 100%" /> |
| 64 | + </el-form-item> |
| 65 | + </el-col> |
| 66 | + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
| 67 | + <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> |
| 68 | + <el-input |
| 69 | + v-model="loginData.loginForm.tenantName" |
| 70 | + :placeholder="t('login.tenantNamePlaceholder')" |
| 71 | + :prefix-icon="iconHouse" |
| 72 | + link |
| 73 | + type="primary" |
| 74 | + /> |
| 75 | + </el-form-item> |
| 76 | + </el-col> |
| 77 | + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
| 78 | + <el-form-item prop="username"> |
| 79 | + <el-input |
| 80 | + v-model="loginData.loginForm.username" |
| 81 | + :placeholder="t('login.usernamePlaceholder')" |
| 82 | + :prefix-icon="iconAvatar" |
| 83 | + /> |
| 84 | + </el-form-item> |
| 85 | + </el-col> |
| 86 | + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
| 87 | + <el-form-item prop="password"> |
| 88 | + <el-input |
| 89 | + v-model="loginData.loginForm.password" |
| 90 | + :placeholder="t('login.passwordPlaceholder')" |
| 91 | + :prefix-icon="iconLock" |
| 92 | + show-password |
| 93 | + type="password" |
| 94 | + @keyup.enter="getCode()" |
| 95 | + /> |
| 96 | + </el-form-item> |
| 97 | + </el-col> |
| 98 | + <el-col |
| 99 | + :span="24" |
| 100 | + style=" |
| 101 | + padding-right: 10px; |
| 102 | + padding-left: 10px; |
| 103 | + margin-top: -20px; |
| 104 | + margin-bottom: -20px; |
| 105 | + " |
| 106 | + > |
| 107 | + <el-form-item> |
| 108 | + <el-row justify="space-between" style="width: 100%"> |
| 109 | + <el-col :span="6"> |
| 110 | + <el-checkbox v-model="loginData.loginForm.rememberMe"> |
| 111 | + {{ t('login.remember') }} |
| 112 | + </el-checkbox> |
| 113 | + </el-col> |
| 114 | + <el-col :offset="6" :span="12"> |
| 115 | + <el-link style="float: right" type="primary">{{ |
| 116 | + t('login.forgetPassword') |
| 117 | + }}</el-link> |
| 118 | + </el-col> |
| 119 | + </el-row> |
| 120 | + </el-form-item> |
| 121 | + </el-col> |
| 122 | + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
| 123 | + <el-form-item> |
| 124 | + <XButton |
| 125 | + :loading="loginLoading" |
| 126 | + :title="t('login.login')" |
| 127 | + class="w-[100%]" |
| 128 | + type="primary" |
| 129 | + @click="getCode()" |
| 130 | + /> |
| 131 | + </el-form-item> |
| 132 | + </el-col> |
| 133 | + <Verify |
| 134 | + ref="verify" |
| 135 | + :captchaType="captchaType" |
| 136 | + :imgSize="{ width: '400px', height: '200px' }" |
| 137 | + mode="pop" |
| 138 | + @success="handleLogin" |
| 139 | + /> |
| 140 | + </el-row> |
| 141 | + </el-form> |
| 142 | + </div> |
| 143 | + </Transition> |
| 144 | + </div> |
| 145 | + </div> |
| 146 | + </div> |
| 147 | +</template> |
| 148 | + |
| 149 | +<script lang="ts" setup> |
| 150 | +import { underlineToHump } from '@/utils' |
| 151 | +
|
| 152 | +import { ElLoading } from 'element-plus' |
| 153 | +
|
| 154 | +import { useDesign } from '@/hooks/web/useDesign' |
| 155 | +import { useAppStore } from '@/store/modules/app' |
| 156 | +import { useIcon } from '@/hooks/web/useIcon' |
| 157 | +import { usePermissionStore } from '@/store/modules/permission' |
| 158 | +
|
| 159 | +import * as LoginApi from '@/api/login' |
| 160 | +import * as authUtil from '@/utils/auth' |
| 161 | +import { ThemeSwitch } from '@/layout/components/ThemeSwitch' |
| 162 | +import { LocaleDropdown } from '@/layout/components/LocaleDropdown' |
| 163 | +import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin' |
| 164 | +import LoginFormTitle from './components/LoginFormTitle.vue' |
| 165 | +import router from '@/router' |
| 166 | +
|
| 167 | +defineOptions({ name: 'SocialLogin' }) |
| 168 | +
|
| 169 | +const { t } = useI18n() |
| 170 | +const route = useRoute() |
| 171 | +
|
| 172 | +const appStore = useAppStore() |
| 173 | +const { getPrefixCls } = useDesign() |
| 174 | +const prefixCls = getPrefixCls('login') |
| 175 | +const iconHouse = useIcon({ icon: 'ep:house' }) |
| 176 | +const iconAvatar = useIcon({ icon: 'ep:avatar' }) |
| 177 | +const iconLock = useIcon({ icon: 'ep:lock' }) |
| 178 | +const formLogin = ref<any>() |
| 179 | +const { validForm } = useFormValid(formLogin) |
| 180 | +const { getLoginState } = useLoginState() |
| 181 | +const { push } = useRouter() |
| 182 | +const permissionStore = usePermissionStore() |
| 183 | +const loginLoading = ref(false) |
| 184 | +const verify = ref() |
| 185 | +const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 |
| 186 | +
|
| 187 | +const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN) |
| 188 | +
|
| 189 | +const LoginRules = { |
| 190 | + tenantName: [required], |
| 191 | + username: [required], |
| 192 | + password: [required] |
| 193 | +} |
| 194 | +const loginData = reactive({ |
| 195 | + isShowPassword: false, |
| 196 | + captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, |
| 197 | + tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, |
| 198 | + loginForm: { |
| 199 | + tenantName: '芋道源码', |
| 200 | + username: 'admin', |
| 201 | + password: 'admin123', |
| 202 | + captchaVerification: '', |
| 203 | + rememberMe: false |
| 204 | + } |
| 205 | +}) |
| 206 | +
|
| 207 | +// 获取验证码 |
| 208 | +const getCode = async () => { |
| 209 | + // 情况一,未开启:则直接登录 |
| 210 | + if (loginData.captchaEnable === 'false') { |
| 211 | + await handleLogin({}) |
| 212 | + } else { |
| 213 | + // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录 |
| 214 | + // 弹出验证码 |
| 215 | + verify.value.show() |
| 216 | + } |
| 217 | +} |
| 218 | +//获取租户ID |
| 219 | +const getTenantId = async () => { |
| 220 | + if (loginData.tenantEnable === 'true') { |
| 221 | + const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName) |
| 222 | + authUtil.setTenantId(res) |
| 223 | + } |
| 224 | +} |
| 225 | +// 记住我 |
| 226 | +const getCookie = () => { |
| 227 | + const loginForm = authUtil.getLoginForm() |
| 228 | + if (loginForm) { |
| 229 | + loginData.loginForm = { |
| 230 | + ...loginData.loginForm, |
| 231 | + username: loginForm.username ? loginForm.username : loginData.loginForm.username, |
| 232 | + password: loginForm.password ? loginForm.password : loginData.loginForm.password, |
| 233 | + rememberMe: loginForm.rememberMe ? true : false, |
| 234 | + tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName |
| 235 | + } |
| 236 | + } |
| 237 | +} |
| 238 | +const loading = ref() // ElLoading.service 返回的实例 |
| 239 | +
|
| 240 | +// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode,需要在回调后进行decode |
| 241 | +function getUrlValue(key: string): string { |
| 242 | + const url = new URL(decodeURIComponent(location.href)) |
| 243 | + return url.searchParams.get(key) ?? '' |
| 244 | +} |
| 245 | +
|
| 246 | +// 尝试登录: 当账号已经绑定,socialLogin会直接获得token |
| 247 | +const tryLogin = async () => { |
| 248 | + try { |
| 249 | + const type = getUrlValue('type') |
| 250 | + const redirect = getUrlValue('redirect') |
| 251 | + const code = route?.query?.code as string |
| 252 | + const state = route?.query?.state as string |
| 253 | +
|
| 254 | + const res = await LoginApi.socialLogin(type, code, state) |
| 255 | + authUtil.setToken(res) |
| 256 | +
|
| 257 | + router.push({ path: redirect || '/' }) |
| 258 | + } catch (err) {} |
| 259 | +} |
| 260 | +
|
| 261 | +// 登录 |
| 262 | +const handleLogin = async (params) => { |
| 263 | + loginLoading.value = true |
| 264 | + try { |
| 265 | + await getTenantId() |
| 266 | + const data = await validForm() |
| 267 | + if (!data) { |
| 268 | + return |
| 269 | + } |
| 270 | +
|
| 271 | + let redirect = getUrlValue('redirect') |
| 272 | +
|
| 273 | + const type = getUrlValue('type') |
| 274 | + const code = route?.query?.code as string |
| 275 | + const state = route?.query?.state as string |
| 276 | +
|
| 277 | + const res = await LoginApi.login({ |
| 278 | + // 账号密码登录 |
| 279 | + username: loginData.loginForm.username, |
| 280 | + password: loginData.loginForm.password, |
| 281 | + captchaVerification: params.captchaVerification, |
| 282 | + // 社交登录 |
| 283 | + socialCode: code, |
| 284 | + socialState: state, |
| 285 | + socialType: type |
| 286 | + }) |
| 287 | + if (!res) { |
| 288 | + return |
| 289 | + } |
| 290 | + loading.value = ElLoading.service({ |
| 291 | + lock: true, |
| 292 | + text: '正在加载系统中...', |
| 293 | + background: 'rgba(0, 0, 0, 0.7)' |
| 294 | + }) |
| 295 | + if (loginData.loginForm.rememberMe) { |
| 296 | + authUtil.setLoginForm(loginData.loginForm) |
| 297 | + } else { |
| 298 | + authUtil.removeLoginForm() |
| 299 | + } |
| 300 | + authUtil.setToken(res) |
| 301 | + if (!redirect) { |
| 302 | + redirect = '/' |
| 303 | + } |
| 304 | + // 判断是否为SSO登录 |
| 305 | + if (redirect.indexOf('sso') !== -1) { |
| 306 | + window.location.href = window.location.href.replace('/login?redirect=', '') |
| 307 | + } else { |
| 308 | + push({ path: redirect || permissionStore.addRouters[0].path }) |
| 309 | + } |
| 310 | + } finally { |
| 311 | + loginLoading.value = false |
| 312 | + loading.value.close() |
| 313 | + } |
| 314 | +} |
| 315 | +
|
| 316 | +onMounted(() => { |
| 317 | + getCookie() |
| 318 | + tryLogin() |
| 319 | +}) |
| 320 | +</script> |
| 321 | + |
| 322 | +<style lang="scss" scoped> |
| 323 | +$prefix-cls: #{$namespace}-login; |
| 324 | +
|
| 325 | +.#{$prefix-cls} { |
| 326 | + overflow: auto; |
| 327 | +
|
| 328 | + &__left { |
| 329 | + &::before { |
| 330 | + position: absolute; |
| 331 | + top: 0; |
| 332 | + left: 0; |
| 333 | + z-index: -1; |
| 334 | + width: 100%; |
| 335 | + height: 100%; |
| 336 | + background-image: url('@/assets/svgs/login-bg.svg'); |
| 337 | + background-position: center; |
| 338 | + background-repeat: no-repeat; |
| 339 | + content: ''; |
| 340 | + } |
| 341 | + } |
| 342 | +} |
| 343 | +</style> |
0 commit comments