Files
Yi.Framework/Yi.Ai.Vue3/src/components/LoginDialog/index.vue
2025-07-15 00:54:34 +08:00

579 lines
17 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 { ElMessageBox } from 'element-plus';
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_LOGIN,
SSO_CLIENT_LOGIN_AGAIN,
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(type: any) {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
console.log('cccc', type);
const popup = window.open(
`${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`,
'SSOLogin',
'width=1000,height=800',
);
// 使用标志位防止重复执行
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('登录超时');
}
}, 60 * 1000); // 60分钟超时关闭
}
// 让意社区重新登录,先让意社区退出登录,再重新登录
function handleLoginAgainYi() {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
const popup = window.open(
`http://localhost:18001/login?client_id=${SSO_CLIENT_LOGIN_AGAIN}&redirect_uri=${redirectUri}`,
'SSOLogin',
'width=1000,height=800',
);
// 使用标志位防止重复执行
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;
console.log('111');
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('登录超时');
}
}, 60 * 1000); // 60分钟超时关闭
}
const wxSrc = computed(
() => `${import.meta.env.VITE_WEB_BASE_API}/wwwroot/aihub/wx.png`,
);
// 微信群二维码
const wxGroupQD = `${import.meta.env.VITE_WEB_BASE_API}/wwwroot/aihub/jlq.png`;
function openContact() {
ElMessageBox.alert(
`
<div class="text-center relative">
<h3 class="text-lg font-bold mb-3">请扫码或搜索微信号添加站长微信<br>
获取专属客服支持</h3>
<div class="mb-4 flex items-center justify-center space-x-2">
<span class="font-semibold">站长微信账号:</span>
<span class="text-blue-600 font-mono select-text" id="wechat-id">chengzilaoge520</span>
<span class="cursor-pointer" onclick="navigator.clipboard.writeText('chengzilaoge520').then(() => { window.parent.ElMessage({
message: '微信号已复制到剪贴板',
type: 'success',
duration: 2000,
});})"
title="点击复制">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 opacity-70 hover:opacity-100" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v16h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 18H8V7h11v16z"/>
</svg>
</span>
</div>
<div class="flex justify-center mb-4">
<div>
<h4>
站长微信
</h4>
<img
src="${wxSrc.value}"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wechat-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
</div><div>
<h4>
微信交流群
</h4>
<img
src="${wxGroupQD}"
class="w-50 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105"
onclick="document.getElementById('wx-group-qrcode-fullscreen').style.display = 'flex'"
alt="微信二维码"
>
</div>
</div>
<div class="text-sm text-gray-600">
<p class="mb-1">请备注 <span class="inline-block bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded text-xs">ai</span> 快速通过验证</p>
</div>
<!-- 全屏放大二维码 -->
<div
id="wechat-qrcode-fullscreen"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center;"
onclick="this.style.display='none'"
>
<img
src="${wxSrc.value}"
style="max-width:90%; max-height:90%; border:8px solid white; border-radius:16px; box-shadow:0 0 40px rgba(255,255,255,0.2);"
/>
</div>
<div
id="wx-group-qrcode-fullscreen"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; justify-content:center; align-items:center;"
onclick="this.style.display='none'"
>
<img
src="${wxGroupQD}"
style="max-width:90%; max-height:90%; border:8px solid white; border-radius:16px; box-shadow:0 0 40px rgba(255,255,255,0.2);"
/>
</div>
</div>
`,
'联系站长',
{
confirmButtonText: '我知道了',
dangerouslyUseHTMLString: true,
customClass: 'wechat-message-box',
callback: () => {
},
},
);
}
</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">
<p class="w-max">
点击下方图标登录
</p>
</el-divider>
<div class="third-party-buttons">
<el-tooltip content="使用意社区账号登录" placement="top">
<div class="third-party-btn" @click="handleThirdPartyLogin(SSO_CLIENT_LOGIN)">
<img :src="logoPng" class="third-party-icon" alt="">
</div>
</el-tooltip>
</div>
<el-divider content-position="center">
<p class="w-max">
开通Vip后点击下方重新登录意社区
</p>
</el-divider>
<el-button
class="w-full"
type="primary"
size="large"
@click="handleThirdPartyLogin(SSO_CLIENT_LOGIN_AGAIN)"
>
意社区重新登录
</el-button>
<el-divider class="w-max">
<p class="w-max">
如遇问题请联系我们
</p>
</el-divider>
<el-button
class="w-full"
type="primary"
size="large"
style="background: #D7BD8D;color: white;border: #191919"
@click="openContact"
>
联系我们
</el-button>
</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: 66px;
height: 66px;
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>