Files
Yi.Admin/Yi.Ai.Vue3/src/components/LoginDialog/index.vue
2025-06-29 00:57:57 +08:00

399 lines
10 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.
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api';
import logoPng from '@/assets/images/logo.png';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { SSO_CLIENT_ID, SSO_SEVER_URL } from '@/config/sso.ts';
import { useUserStore } from '@/stores';
import { useLoginFormStore } from '@/stores/modules/loginForm';
import { useSessionStore } from '@/stores/modules/session.ts';
import RegistrationForm from './components/FormLogin/RegistrationForm.vue';
import QrCodeLogin from './components/QrCodeLogin/index.vue';
const loginFromStore = useLoginFormStore();
const loginFormType = computed(() => loginFromStore.LoginFormType);
// 使用 defineModel 定义双向绑定的 visible需 Vue 3.4+
const visible = defineModel<boolean>('visible');
const showMask = ref(false); // 控制遮罩层显示的独立状态
const isQrMode = ref(false);
const userStore = useUserStore();
const router = useRouter();
const sessionStore = useSessionStore();
// 监听 visible 变化,控制遮罩层显示时机
watch(
visible,
(newVal) => {
if (newVal) {
// 恢复默认
isQrMode.value = false;
// 显示时立即展示遮罩
showMask.value = true;
}
},
{ immediate: true },
);
// 切换二维码登录
function toggleLoginMode() {
isQrMode.value = !isQrMode.value;
}
// 点击遮罩层关闭对话框(触发过渡动画)
function handleMaskClick() {
// 触发离开动画
userStore.closeLoginDialog();
}
// 过渡动画结束回调
function onAfterLeave() {
if (!visible.value) {
showMask.value = false; // 动画结束后隐藏遮罩
}
}
function handleThirdPartyLogin() {
console.log('SSO_SEVER_URL', SSO_SEVER_URL);
console.log('import.meta.env', import.meta.env);
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
const popup = window.open(
`${SSO_SEVER_URL}/login?client_id=${SSO_CLIENT_ID}&redirect_uri=${redirectUri}`,
'SSOLogin',
'width=800,height=600',
);
// 使用标志位防止重复执行
let isHandled = false;
const messageHandler = async (event: any) => {
if (event.origin === new URL(SSO_SEVER_URL).origin
&& event.data.type === 'SSO_LOGIN_SUCCESS'
&& !isHandled) {
isHandled = true;
try {
// 清理监听
window.removeEventListener('message', messageHandler);
const { token, refreshToken } = event.data;
userStore.setToken(token, refreshToken);
const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data);
// 关闭弹窗
if (popup && !popup.closed)
popup.close();
// 后续逻辑
ElMessage.success('登录成功');
userStore.closeLoginDialog();
await sessionStore.requestSessionList(1, true);
await router.replace('/');
}
catch (error) {
console.error('登录处理失败:', error);
ElMessage.error('登录失败');
}
}
};
// 先移除旧监听,再添加新监听
window.removeEventListener('message', messageHandler);
window.addEventListener('message', messageHandler);
// 超时自动清理
setTimeout(() => {
if (!isHandled) {
window.removeEventListener('message', messageHandler);
if (popup && !popup.closed)
popup.close();
ElMessage.warning('登录超时');
}
}, 30000); // 30秒超时
}
</script>
<template>
<!-- 使用 Teleport 将内容传送至 body -->
<Teleport to="body">
<div v-show="showMask" class="mask" @click.self="handleMaskClick">
<!-- 仅对弹框应用过渡动画 -->
<Transition name="dialog-zoom" @after-leave="onAfterLeave">
<div v-show="visible" class="glass-dialog">
<div class="left-section">
<div class="logo-wrap">
<img :src="logoPng" class="logo-img">
<span class="logo-text">意心-Ai</span>
</div>
<div class="ad-banner">
<SvgIcon name="p-bangong" class-name="animate-up-down" />
</div>
</div>
<div class="right-section">
<!-- 隐藏二维码登录 -->
<div v-if="false" class="mode-toggle" @click.stop="toggleLoginMode">
<SvgIcon v-if="!isQrMode" name="erweimadenglu" />
<SvgIcon v-else name="zhanghaodenglu" />
</div>
<div class="content-wrapper">
<div v-if="!isQrMode" class="form-box">
<!-- 表单容器父组件可以自定定义表单插槽 -->
<slot name="form">
<!-- 父组件不用插槽则显示默认表单 默认使用 AccountPassword 组件 -->
<div v-if="loginFormType === 'AccountPassword'" class="form-container">
<span class="content-title"> 登录后免费使用完整功能 </span>
<el-divider v-if="false" content-position="center">
账号密码登录
</el-divider>
<AccountPassword v-if="false" />
<!-- 新增第三方登录按钮 -->
<div class="third-party-login">
<el-divider content-position="center">
点击下方登录
</el-divider>
<div class="third-party-buttons">
<el-tooltip content="使用意社区账号登录" placement="top">
<div class="third-party-btn" @click="handleThirdPartyLogin">
<img :src="logoPng" class="third-party-icon" alt="">
</div>
</el-tooltip>
</div>
</div>
</div>
<div v-if="loginFormType === 'RegistrationForm'" class="form-container">
<span class="content-title"> 登录后免费使用完整功能 </span>
<el-divider content-position="center">
邮箱注册账号
</el-divider>
<RegistrationForm />
</div>
</slot>
</div>
<div v-else class="qr-container">
<QrCodeLogin />
</div>
</div>
</div>
</div>
</Transition>
</div>
</Teleport>
</template>
<style scoped lang="scss">
/* 动画样式(仅作用于弹框) */
.dialog-zoom-enter-active,
.dialog-zoom-leave-active {
transition: all 0.3s ease-in-out;
transform-origin: center;
}
.dialog-zoom-enter-from,
.dialog-zoom-leave-to {
opacity: 0;
transform: scale(0.8);
}
.dialog-zoom-enter-to,
.dialog-zoom-leave-from {
opacity: 1;
transform: scale(1);
}
/* 遮罩层样式 */
.mask {
position: fixed;
top: 0;
left: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
overflow: hidden;
user-select: none;
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(3px);
opacity: 1;
transition: opacity 0.3s;
}
.mask[hidden] {
opacity: 0;
}
/* 对话框容器样式 */
.glass-dialog {
z-index: 1000;
display: flex;
width: fit-content;
max-width: 90%;
height: var(--login-dialog-height);
padding: var(--login-dialog-padding);
overflow: hidden;
background-color: #ffffff;
border-radius: var(--login-dialog-border-radius);
box-shadow: 0 4px 24px rgb(0 0 0 / 10%);
}
/* 以下样式与原代码一致,未修改 */
.left-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: calc(var(--login-dialog-width) / 2);
padding: var(--login-dialog-section-padding);
background: linear-gradient(
233deg,
rgb(113 161 255 / 60%) 17.67%,
rgb(154 219 255 / 60%) 70.4%
);
}
.left-section .logo-wrap {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 24px;
}
.left-section .logo-wrap .logo-img {
width: 40px;
height: 40px;
padding: 4px;
background: var(--login-dialog-logo-background);
filter: drop-shadow(0 4px 4px rgb(0 0 0 / 10%));
border-radius: 12px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 8%);
}
.left-section .logo-wrap .logo-text {
font-size: 16px;
font-weight: 600;
color: var(--login-dialog-logo-text-color);
}
.left-section .ad-banner {
position: relative;
width: 100%;
height: 100%;
}
.left-section .ad-banner .svg-icon {
position: absolute;
width: 100%;
height: 310px;
}
.right-section {
position: relative;
display: flex;
flex-direction: column;
width: calc(var(--login-dialog-width) / 2);
padding: var(--login-dialog-section-padding);
}
.right-section .content-wrapper {
flex: 1;
padding: 8px 0;
overflow: hidden;
}
.right-section .content-title {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
font-size: 20px;
font-weight: 700;
}
.right-section .mode-toggle {
position: absolute;
top: 16px;
right: 16px;
font-size: 24px;
color: var(--login-dialog-mode-toggle-color);
cursor: pointer;
transition: color 0.3s;
}
.right-section .form-container,
.right-section .qr-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.right-section .form-box {
place-self: center center;
width: 260px;
height: 100%;
padding: var(--login-dialog-section-padding);
border-radius: var(--login-dialog-border-radius);
}
/* 新增:第三方登录样式 */
.third-party-login {
width: 100%;
margin-top: 20px;
}
.third-party-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 10px;
}
.third-party-btn {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
cursor: pointer;
background-color: #f5f7fa;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background-color: #e4e7ed;
transform: scale(1.1);
}
}
.third-party-icon {
width: 30px;
height: 30px;
border-radius: 50%;
}
@media (width <= 800px) {
.left-section {
display: none !important;
}
.glass-dialog {
height: var(--login-dialog-height);
padding: var(--login-dialog-padding);
}
.right-section {
padding: calc(var(--login-dialog-section-padding) - 8px);
}
.content-wrapper {
padding: 4px 0;
}
}
.animate-up-down {
animation: up-down 5s linear 0ms infinite;
}
@keyframes up-down {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0);
}
}
</style>