feat: ai-hub与bbs单点登录联通
This commit is contained in:
@@ -12,6 +12,11 @@ VITE_WEB_BASE_API = '/dev-api'
|
|||||||
|
|
||||||
# 本地接口
|
# 本地接口
|
||||||
# VITE_API_URL = http://122.51.75.95:6039
|
# VITE_API_URL = http://122.51.75.95:6039
|
||||||
VITE_API_URL = http://129.211.24.7:6039
|
VITE_API_URL=http://129.211.24.7:6039
|
||||||
# VITE_API_URL = https://ccnetcore.com
|
# VITE_API_URL = https://ccnetcore.com
|
||||||
# VITE_API_URL = https://mp.pandarobot.chat
|
# VITE_API_URL = https://mp.pandarobot.chat
|
||||||
|
|
||||||
|
# SSO单点登录url
|
||||||
|
SSO_SEVER_URL='http://localhost:18001'
|
||||||
|
# SSO单点登录项目标识
|
||||||
|
SSO_CLIENT_ID='YiXin-Ai';
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ VITE_BUILD_COMPRESS = gzip
|
|||||||
# VITE_API_URL = http://122.51.75.95:6039
|
# VITE_API_URL = http://122.51.75.95:6039
|
||||||
# VITE_API_URL = http://129.211.24.7:6039
|
# VITE_API_URL = http://129.211.24.7:6039
|
||||||
VITE_API_URL = https://mp.pandarobot.chat
|
VITE_API_URL = https://mp.pandarobot.chat
|
||||||
|
# SSO单点登录url
|
||||||
|
SSO_SEVER_URL='https://ccnetcore.com'
|
||||||
|
# SSO单点登录项目标识
|
||||||
|
SSO_CLIENT_ID='YiXin-Ai';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ChatMessageVo, GetChatListParams, SendDTO } from './types';
|
|||||||
import { get, post } from '@/utils/request';
|
import { get, post } from '@/utils/request';
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
export const send = (data: SendDTO) => post<null>('/prod-api/ai/send', data);
|
export const send = (data: SendDTO) => post<null>('/prod-api/ai-chat/send', data);
|
||||||
// export const send = (data: SendDTO) => post<null>('/chat/send', data);
|
// export const send = (data: SendDTO) => post<null>('/chat/send', data);
|
||||||
|
|
||||||
// 新增对应会话聊天记录
|
// 新增对应会话聊天记录
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from './auth';
|
|||||||
export * from './chat';
|
export * from './chat';
|
||||||
export * from './model';
|
export * from './model';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
|
export * from './user';
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import { get } from '@/utils/request';
|
|||||||
// 获取当前用户的模型列表
|
// 获取当前用户的模型列表
|
||||||
export function getModelList() {
|
export function getModelList() {
|
||||||
// return get<GetSessionListVO[]>('/system/model/modelList');
|
// return get<GetSessionListVO[]>('/system/model/modelList');
|
||||||
return get<GetSessionListVO[]>('/prod-api/ai/model');
|
return get<GetSessionListVO[]>('/prod-api/ai-chat/model');
|
||||||
}
|
}
|
||||||
|
|||||||
6
Yi.Ai.Vue3/src/api/user/index.ts
Normal file
6
Yi.Ai.Vue3/src/api/user/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { get } from '@/utils/request';
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
export function getUserInfo() {
|
||||||
|
return get<any>('/prod-api/account');
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { getUserInfo } from '@/api';
|
||||||
import logoPng from '@/assets/images/logo.png';
|
import logoPng from '@/assets/images/logo.png';
|
||||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
|
import { SSO_CLIENT_ID, SSO_SEVER_URL } from '@/config/sso.ts';
|
||||||
import { useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
import { useLoginFormStore } from '@/stores/modules/loginForm';
|
import { useLoginFormStore } from '@/stores/modules/loginForm';
|
||||||
import AccountPassword from './components/FormLogin/AccountPassword.vue';
|
import { useSessionStore } from '@/stores/modules/session.ts';
|
||||||
import RegistrationForm from './components/FormLogin/RegistrationForm.vue';
|
import RegistrationForm from './components/FormLogin/RegistrationForm.vue';
|
||||||
import QrCodeLogin from './components/QrCodeLogin/index.vue';
|
import QrCodeLogin from './components/QrCodeLogin/index.vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const loginFromStore = useLoginFormStore();
|
const loginFromStore = useLoginFormStore();
|
||||||
|
|
||||||
const loginFormType = computed(() => loginFromStore.LoginFormType);
|
const loginFormType = computed(() => loginFromStore.LoginFormType);
|
||||||
@@ -17,6 +19,9 @@ const loginFormType = computed(() => loginFromStore.LoginFormType);
|
|||||||
const visible = defineModel<boolean>('visible');
|
const visible = defineModel<boolean>('visible');
|
||||||
const showMask = ref(false); // 控制遮罩层显示的独立状态
|
const showMask = ref(false); // 控制遮罩层显示的独立状态
|
||||||
const isQrMode = ref(false);
|
const isQrMode = ref(false);
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
// 监听 visible 变化,控制遮罩层显示时机
|
// 监听 visible 变化,控制遮罩层显示时机
|
||||||
watch(
|
watch(
|
||||||
@@ -49,6 +54,68 @@ function onAfterLeave() {
|
|||||||
showMask.value = false; // 动画结束后隐藏遮罩
|
showMask.value = false; // 动画结束后隐藏遮罩
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleThirdPartyLogin() {
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isHandled = true;
|
||||||
|
try {
|
||||||
|
const { token } = event.data;
|
||||||
|
userStore.setToken(token);
|
||||||
|
|
||||||
|
const resUserInfo = await getUserInfo();
|
||||||
|
userStore.setUserInfo(resUserInfo.data);
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
if (popup && !popup.closed)
|
||||||
|
popup.close();
|
||||||
|
|
||||||
|
// 清理监听
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// 后续逻辑
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -61,14 +128,15 @@ function onAfterLeave() {
|
|||||||
<div class="left-section">
|
<div class="left-section">
|
||||||
<div class="logo-wrap">
|
<div class="logo-wrap">
|
||||||
<img :src="logoPng" class="logo-img">
|
<img :src="logoPng" class="logo-img">
|
||||||
<span class="logo-text">Element Plus X</span>
|
<span class="logo-text">YiXin-Ai</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ad-banner">
|
<div class="ad-banner">
|
||||||
<SvgIcon name="p-bangong" class-name="animate-up-down" />
|
<SvgIcon name="p-bangong" class-name="animate-up-down" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-section">
|
<div class="right-section">
|
||||||
<div class="mode-toggle" @click.stop="toggleLoginMode">
|
<!-- 隐藏二维码登录 -->
|
||||||
|
<div v-if="false" class="mode-toggle" @click.stop="toggleLoginMode">
|
||||||
<SvgIcon v-if="!isQrMode" name="erweimadenglu" />
|
<SvgIcon v-if="!isQrMode" name="erweimadenglu" />
|
||||||
<SvgIcon v-else name="zhanghaodenglu" />
|
<SvgIcon v-else name="zhanghaodenglu" />
|
||||||
</div>
|
</div>
|
||||||
@@ -80,11 +148,25 @@ function onAfterLeave() {
|
|||||||
<div v-if="loginFormType === 'AccountPassword'" class="form-container">
|
<div v-if="loginFormType === 'AccountPassword'" class="form-container">
|
||||||
<span class="content-title"> 登录后免费使用完整功能 </span>
|
<span class="content-title"> 登录后免费使用完整功能 </span>
|
||||||
|
|
||||||
<el-divider content-position="center">
|
<el-divider v-if="false" content-position="center">
|
||||||
账号密码登录
|
账号密码登录
|
||||||
</el-divider>
|
</el-divider>
|
||||||
|
|
||||||
<AccountPassword />
|
<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>
|
||||||
|
|
||||||
<div v-if="loginFormType === 'RegistrationForm'" class="form-container">
|
<div v-if="loginFormType === 'RegistrationForm'" class="form-container">
|
||||||
@@ -254,6 +336,41 @@ function onAfterLeave() {
|
|||||||
border-radius: var(--login-dialog-border-radius);
|
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) {
|
@media (width <= 800px) {
|
||||||
.left-section {
|
.left-section {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
4
Yi.Ai.Vue3/src/config/sso.ts
Normal file
4
Yi.Ai.Vue3/src/config/sso.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// 三方登录
|
||||||
|
// export const SSO_SEVER_URL: string = 'https://ccnetcore.com';
|
||||||
|
export const SSO_SEVER_URL: string = import.meta.env.SSO_SEVER_URL || 'http://localhost:18001';
|
||||||
|
export const SSO_CLIENT_ID: string = import.meta.env.SSO_CLIENT_ID || 'YiXin-Ai';
|
||||||
@@ -15,7 +15,7 @@ export const useUserStore = defineStore(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const userInfo = ref<LoginUser>();
|
const userInfo = ref<LoginUser>();
|
||||||
const setUserInfo = (value: LoginUser) => {
|
const setUserInfo = (value: any) => {
|
||||||
userInfo.value = value;
|
userInfo.value = value;
|
||||||
};
|
};
|
||||||
const clearUserInfo = () => {
|
const clearUserInfo = () => {
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ import hookFetch from 'hook-fetch';
|
|||||||
import { sseTextDecoderPlugin } from 'hook-fetch/plugins';
|
import { sseTextDecoderPlugin } from 'hook-fetch/plugins';
|
||||||
import router from '@/routers';
|
import router from '@/routers';
|
||||||
import { useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
|
// 标准响应格式
|
||||||
interface BaseResponse {
|
// 标准响应格式
|
||||||
|
interface BaseResponse<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
data: never;
|
data: T;
|
||||||
msg: string;
|
msg: string;
|
||||||
rows: never;
|
|
||||||
}
|
}
|
||||||
|
// 扩展请求函数类型声明
|
||||||
// 修改 BaseResponse 类型,使其兼容裸数组
|
declare module 'hook-fetch' {
|
||||||
type BaseResponse<T = any> =
|
interface HookFetchDefaults {
|
||||||
| { code: number; data: T; msg: string } // 标准格式
|
// 允许响应是裸数据(自动会被插件包装)
|
||||||
| T[]; // 裸数组格式
|
response: any;
|
||||||
|
}
|
||||||
export const request = hookFetch.create<BaseResponse, 'data' | 'rows'>({
|
}
|
||||||
|
export const request = hookFetch.create<BaseResponse>({
|
||||||
// baseURL: import.meta.env.VITE_API_URL,
|
// baseURL: import.meta.env.VITE_API_URL,
|
||||||
baseURL: '', // 留空或使用'/'
|
baseURL: '', // 留空或使用'/'
|
||||||
headers: {
|
headers: {
|
||||||
@@ -28,18 +29,20 @@ export const request = hookFetch.create<BaseResponse, 'data' | 'rows'>({
|
|||||||
{
|
{
|
||||||
name: 'adapt-array-response',
|
name: 'adapt-array-response',
|
||||||
afterResponse: async (response) => {
|
afterResponse: async (response) => {
|
||||||
// 如果是数组格式,手动包裹成 { data: [...] }
|
// 已经是标准格式(包含 code 字段)
|
||||||
if (Array.isArray(response.result)) {
|
if (typeof response.result?.code === 'number') {
|
||||||
return {
|
return response;
|
||||||
...response,
|
|
||||||
result: {
|
|
||||||
code: 200,
|
|
||||||
msg: 'success',
|
|
||||||
data: response.result,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
|
// 非标准格式 → 包装为标准格式
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
result: {
|
||||||
|
code: 200, // 默认成功码
|
||||||
|
data: response.result, // 原始数据放入 data
|
||||||
|
msg: 'success', // 默认消息
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
7
Yi.Ai.Vue3/types/components.d.ts
vendored
7
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -12,9 +12,6 @@ declare module 'vue' {
|
|||||||
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
|
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
@@ -25,9 +22,7 @@ declare module 'vue' {
|
|||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
|
||||||
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
|
||||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||||
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
|
||||||
|
|||||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -5,7 +5,6 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_WEB_TITLE_EN: string;
|
readonly VITE_WEB_TITLE_EN: string;
|
||||||
readonly VITE_WEB_ENV: string;
|
readonly VITE_WEB_ENV: string;
|
||||||
readonly VITE_WEB_BASE_API: string;
|
readonly VITE_WEB_BASE_API: string;
|
||||||
readonly VITE_BUILD_COMPRESS: string;
|
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ const whiteList = ["/login", "/auth-redirect", "/bind", "/register"];
|
|||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
const hasToken = getToken();
|
const hasToken = getToken();
|
||||||
|
|
||||||
|
if (to.path === "/login" || to.path === "/index") {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const isPopup = window.opener && window.opener !== window;
|
||||||
|
const clientId = urlParams.get('client_id');
|
||||||
|
const redirectUri = urlParams.get('redirect_uri');
|
||||||
|
|
||||||
|
// 检查是否已经处理过SSO登录(防重复)
|
||||||
|
const hasHandledSSO = sessionStorage.getItem('sso_handled');
|
||||||
|
if (isPopup && clientId && redirectUri && !hasHandledSSO) {
|
||||||
|
if (hasToken) {
|
||||||
|
// 标记已处理,避免重复发送
|
||||||
|
sessionStorage.setItem('sso_handled', 'true');
|
||||||
|
|
||||||
|
// 发送消息给父窗口
|
||||||
|
window.opener.postMessage({
|
||||||
|
type: 'SSO_LOGIN_SUCCESS',
|
||||||
|
token: hasToken
|
||||||
|
}, redirectUri);
|
||||||
|
|
||||||
|
// 关闭弹出窗口
|
||||||
|
setTimeout(() => window.close(), 100);
|
||||||
|
} else {
|
||||||
|
// 存储SSO参数,但改用sessionStorage避免持久化
|
||||||
|
sessionStorage.setItem('sso_params', JSON.stringify({
|
||||||
|
isPopup,
|
||||||
|
clientId,
|
||||||
|
redirectUri
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasToken) {
|
if (hasToken) {
|
||||||
if (to.path === "/login") {
|
if (to.path === "/login") {
|
||||||
// 已经登陆跳转到首页
|
// 已经登陆跳转到首页
|
||||||
|
|||||||
Reference in New Issue
Block a user