diff --git a/src/api/login/index.ts b/src/api/login/index.ts index b65a90cf..1ffb38d6 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -47,6 +47,18 @@ export const smsLogin = (data: SmsLoginVO) => { return request.post({ url: '/system/auth/sms-login', data }) } +// 社交快捷登录,使用 code 授权码 +export function socialLogin(type: string, code: string, state: string) { + return request.post({ + url: '/system/auth/social-login', + data: { + type, + code, + state + } + }) +} + // 社交授权的跳转 export const socialAuthRedirect = (type: number, redirectUri: string) => { return request.get({ diff --git a/src/api/login/types.ts b/src/api/login/types.ts index b2173f72..fff81225 100644 --- a/src/api/login/types.ts +++ b/src/api/login/types.ts @@ -2,6 +2,9 @@ export type UserLoginVO = { username: string password: string captchaVerification: string + socialType?: string + socialCode?: string + socialState?: string } export type TokenType = { diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index cc4bb47e..4f95852f 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -141,6 +141,7 @@ export default { }, router: { login: '登录', + socialLogin: '社交登录', home: '首页', analysis: '分析页', workplace: '工作台' diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index d8172d27..3c4898e4 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -186,12 +186,12 @@ const remainingRouter: AppRouteRecordRaw[] = [ } }, { - path: '/sso', - component: () => import('@/views/Login/Login.vue'), - name: 'SSOLogin', + path: '/social-login', + component: () => import('@/views/Login/SocialLogin.vue'), + name: 'SocialLogin', meta: { hidden: true, - title: t('router.login'), + title: t('router.socialLogin'), noTagsView: true } }, diff --git a/src/views/Login/SocialLogin.vue b/src/views/Login/SocialLogin.vue new file mode 100644 index 00000000..6bbfc1df --- /dev/null +++ b/src/views/Login/SocialLogin.vue @@ -0,0 +1,343 @@ +<template> + <div + :class="prefixCls" + class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px" + > + <div class="relative mx-auto h-full flex"> + <div + :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" + > + <!-- 左上角的 logo + 系统标题 --> + <div class="relative flex items-center text-white"> + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> + </div> + <!-- 左边的背景图 + 欢迎语 --> + <div class="h-[calc(100%-60px)] flex items-center justify-center"> + <TransitionGroup + appear + enter-active-class="animate__animated animate__bounceInLeft" + tag="div" + > + <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> + <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> + <div key="3" class="mt-5 text-14px font-normal text-white"> + {{ t('login.message') }} + </div> + </TransitionGroup> + </div> + </div> + <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px"> + <!-- 右上角的主题、语言选择 --> + <div + class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end" + > + <div class="flex items-center at-2xl:hidden at-xl:hidden"> + <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> + <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> + </div> + <div class="flex items-center justify-end space-x-10px"> + <ThemeSwitch /> + <LocaleDropdown class="dark:text-white lt-xl:text-white" /> + </div> + </div> + <!-- 右边的登录界面 --> + <Transition appear enter-active-class="animate__animated animate__bounceInRight"> + <div + 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" + > + <!-- 账号登录 --> + <el-form + v-show="getShow" + ref="formLogin" + :model="loginData.loginForm" + :rules="LoginRules" + class="login-form" + label-position="top" + label-width="120px" + size="large" + > + <el-row style="margin-right: -10px; margin-left: -10px"> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item> + <LoginFormTitle style="width: 100%" /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> + <el-input + v-model="loginData.loginForm.tenantName" + :placeholder="t('login.tenantNamePlaceholder')" + :prefix-icon="iconHouse" + link + type="primary" + /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item prop="username"> + <el-input + v-model="loginData.loginForm.username" + :placeholder="t('login.usernamePlaceholder')" + :prefix-icon="iconAvatar" + /> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item prop="password"> + <el-input + v-model="loginData.loginForm.password" + :placeholder="t('login.passwordPlaceholder')" + :prefix-icon="iconLock" + show-password + type="password" + @keyup.enter="getCode()" + /> + </el-form-item> + </el-col> + <el-col + :span="24" + style=" + padding-right: 10px; + padding-left: 10px; + margin-top: -20px; + margin-bottom: -20px; + " + > + <el-form-item> + <el-row justify="space-between" style="width: 100%"> + <el-col :span="6"> + <el-checkbox v-model="loginData.loginForm.rememberMe"> + {{ t('login.remember') }} + </el-checkbox> + </el-col> + <el-col :offset="6" :span="12"> + <el-link style="float: right" type="primary">{{ + t('login.forgetPassword') + }}</el-link> + </el-col> + </el-row> + </el-form-item> + </el-col> + <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> + <el-form-item> + <XButton + :loading="loginLoading" + :title="t('login.login')" + class="w-[100%]" + type="primary" + @click="getCode()" + /> + </el-form-item> + </el-col> + <Verify + ref="verify" + :captchaType="captchaType" + :imgSize="{ width: '400px', height: '200px' }" + mode="pop" + @success="handleLogin" + /> + </el-row> + </el-form> + </div> + </Transition> + </div> + </div> + </div> +</template> + +<script lang="ts" setup> +import { underlineToHump } from '@/utils' + +import { ElLoading } from 'element-plus' + +import { useDesign } from '@/hooks/web/useDesign' +import { useAppStore } from '@/store/modules/app' +import { useIcon } from '@/hooks/web/useIcon' +import { usePermissionStore } from '@/store/modules/permission' + +import * as LoginApi from '@/api/login' +import * as authUtil from '@/utils/auth' +import { ThemeSwitch } from '@/layout/components/ThemeSwitch' +import { LocaleDropdown } from '@/layout/components/LocaleDropdown' +import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin' +import LoginFormTitle from './components/LoginFormTitle.vue' +import router from '@/router' + +defineOptions({ name: 'SocialLogin' }) + +const { t } = useI18n() +const route = useRoute() + +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<any>() +const { validForm } = useFormValid(formLogin) +const { getLoginState } = useLoginState() +const { push } = useRouter() +const permissionStore = usePermissionStore() +const loginLoading = ref(false) +const verify = ref() +const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 + +const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN) + +const LoginRules = { + tenantName: [required], + username: [required], + password: [required] +} +const loginData = reactive({ + isShowPassword: false, + captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, + tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, + loginForm: { + tenantName: '芋道源码', + username: 'admin', + password: 'admin123', + captchaVerification: '', + rememberMe: false + } +}) + +// 获取验证码 +const getCode = async () => { + // 情况一,未开启:则直接登录 + if (loginData.captchaEnable === 'false') { + await handleLogin({}) + } else { + // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录 + // 弹出验证码 + verify.value.show() + } +} +//获取租户ID +const getTenantId = async () => { + if (loginData.tenantEnable === 'true') { + const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName) + authUtil.setTenantId(res) + } +} +// 记住我 +const getCookie = () => { + 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 ? true : false, + tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName + } + } +} +const loading = ref() // ElLoading.service 返回的实例 + +// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode,需要在回调后进行decode +function getUrlValue(key: string): string { + const url = new URL(decodeURIComponent(location.href)) + return url.searchParams.get(key) ?? '' +} + +// 尝试登录: 当账号已经绑定,socialLogin会直接获得token +const tryLogin = async () => { + try { + const type = getUrlValue('type') + const redirect = getUrlValue('redirect') + const code = route?.query?.code as string + const state = route?.query?.state as string + + const res = await LoginApi.socialLogin(type, code, state) + authUtil.setToken(res) + + router.push({ path: redirect || '/' }) + } catch (err) {} +} + +// 登录 +const handleLogin = async (params) => { + loginLoading.value = true + try { + await getTenantId() + const data = await validForm() + if (!data) { + return + } + + let redirect = getUrlValue('redirect') + + const type = getUrlValue('type') + const code = route?.query?.code as string + const state = route?.query?.state as string + + const res = await LoginApi.login({ + // 账号密码登录 + username: loginData.loginForm.username, + password: loginData.loginForm.password, + captchaVerification: params.captchaVerification, + // 社交登录 + socialCode: code, + socialState: state, + socialType: type + }) + if (!res) { + return + } + loading.value = ElLoading.service({ + lock: true, + text: '正在加载系统中...', + background: 'rgba(0, 0, 0, 0.7)' + }) + if (loginData.loginForm.rememberMe) { + authUtil.setLoginForm(loginData.loginForm) + } else { + authUtil.removeLoginForm() + } + authUtil.setToken(res) + if (!redirect) { + redirect = '/' + } + // 判断是否为SSO登录 + if (redirect.indexOf('sso') !== -1) { + window.location.href = window.location.href.replace('/login?redirect=', '') + } else { + push({ path: redirect || permissionStore.addRouters[0].path }) + } + } finally { + loginLoading.value = false + loading.value.close() + } +} + +onMounted(() => { + getCookie() + tryLogin() +}) +</script> + +<style lang="scss" scoped> +$prefix-cls: #{$namespace}-login; + +.#{$prefix-cls} { + overflow: auto; + + &__left { + &::before { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; + background-image: url('@/assets/svgs/login-bg.svg'); + background-position: center; + background-repeat: no-repeat; + content: ''; + } + } +} +</style> diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index a4eb0b92..9bee2523 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -284,8 +284,13 @@ const doSocialLogin = async (type: number) => { }) } // 计算 redirectUri + // tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。 + // 配合 Login/SocialLogin.vue#getUrlValue() 使用 const redirectUri = - location.origin + '/social-login?type=' + type + '&redirect=' + (redirect.value || '/') + location.origin + + '/social-login?' + + encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`) + // 进行跳转 const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri)) window.location.href = res