-
@@ -30,13 +37,15 @@ import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
+import ChatPopup from '../layout/components/Aichat/ChatPopup.vue'
import {
initCoze
-} from "@/utils/ai.js";
+} from '@/utils/ai.js'
import {
getAccessToken
-} from "@/api/aiJWT/aiJWT.js"
+} from '@/api/aiJWT/aiJWT.js'
+
export default {
name: 'Layout',
components: {
@@ -45,9 +54,15 @@ export default {
RightPanel,
Settings,
Sidebar,
- TagsView
+ TagsView,
+ ChatPopup // 注册ChatPopup组件
},
mixins: [ResizeMixin],
+ data() {
+ return {
+ showAI: false // 控制AI弹窗显示/隐藏的变量
+ }
+ },
computed: {
...mapState({
theme: state => state.settings.theme,
@@ -70,42 +85,54 @@ export default {
}
},
variables() {
- return variables;
+ return variables
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
- async showAI() {
- let userInfo = {
- roleGroup: this.userInfo.roles[0].roleName || "student",
- nickName: this.userInfo.nickName,
- username: this.userInfo.userName,
- avater: this.avatar,
- user_token: this.token
- }
- console.log("请求AI的信息", userInfo)
-
- //1.获取token
- userInfo.accessToken = (await this.getAccessToken()).access_token;
- userInfo.onRefreshToken = async () => (await this.getAccessToken()).accessToken;
- const sdk = await initCoze(userInfo);
- sdk.showChatBot();
+ // 切换AI弹窗显示状态
+ toggleAI() {
+ this.showAI = !this.showAI
+ // 如果需要在显示时执行原有逻辑,可以取消下面的注释
+ // if (this.showAI) {
+ // this.initializeAI()
+ // }
+ },
+ // 原有AI初始化逻辑,保持注释状态
+ async initializeAI() {
+ // let userInfo = {
+ // roleGroup: this.userInfo.roles[0].roleName || "student",
+ // nickName: this.userInfo.nickName,
+ // username: this.userInfo.userName,
+ // avater: this.avatar,
+ // user_token: this.token
+ // }
+ // console.log("请求AI的信息", userInfo)
+ //
+ // //1.获取token
+ // userInfo.accessToken = (await this.getAccessToken()).access_token;
+ // userInfo.onRefreshToken = async () => (await this.getAccessToken()).accessToken;
+ // const sdk = await initCoze(userInfo);
+ // sdk.showChatBot();
},
async getAccessToken() {
- const res = await getAccessToken(); // 调用请求函数
- const data = JSON.parse(res.data); // 解析数据
- return data; // ✅ 返回 data
+ const res = await getAccessToken() // 调用请求函数
+ const data = JSON.parse(res.data) // 解析数据
+ return data // ✅ 返回 data
}
}
}
+
diff --git a/src/utils/ai_request.js b/src/utils/ai_request.js
new file mode 100644
index 0000000..43f9947
--- /dev/null
+++ b/src/utils/ai_request.js
@@ -0,0 +1,54 @@
+import axios from 'axios'
+import { getTokenKeySessionStorage } from './auth'
+import { useRouter } from 'vue-router'
+import { showToast } from '@/utils/toast' // 请替换为你的Toast组件
+
+// 创建axios实例
+const service = axios.create({
+ baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088',
+ timeout: 15000,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+ config => {
+ // 从本地存储获取token
+ const token = getTokenKeySessionStorage()
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ error => {
+ return Promise.reject(error)
+ }
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+ response => {
+ // 对响应数据做处理
+ return response.data
+ },
+ error => {
+ const router = useRouter()
+
+ // 处理401未授权
+ if (error.response?.status === 401) {
+ showToast('登录已过期,请重新登录', 'error')
+ router.push('/login')
+ }
+
+ // 处理其他错误状态码
+ if (error.response?.status === 500) {
+ showToast('服务器错误,请稍后再试', 'error')
+ }
+
+ return Promise.reject(error)
+ }
+)
+
+export default service
\ No newline at end of file
diff --git a/src/utils/ai_stream.js b/src/utils/ai_stream.js
new file mode 100644
index 0000000..a3c19ca
--- /dev/null
+++ b/src/utils/ai_stream.js
@@ -0,0 +1,115 @@
+import { getTokenKeySessionStorage } from "@/utils/auth";
+
+// 使用环境变量配置基础URL
+const BASE_URL = process.env.VUE_APP_API_BASE_URL || "http://localhost:8088";
+
+/**
+ * 创建聊天流式连接
+ * @param {Object} params 请求参数
+ * @param {string} params.prompt 用户输入
+ * @param {string} params.userId 用户ID
+ * @param {string} params.userName 用户名
+ * @param {string} [params.conversationId] 会话ID
+ * @returns {Object} 包含stream和cancel方法的对象
+ */
+export function createChatStream(params) {
+ const requestId = `req-${Date.now()}-${Math.random()
+ .toString(36)
+ .slice(2, 8)}`;
+ const url = `${BASE_URL}/aitutor/aichat/stream`;
+ const token = getTokenKeySessionStorage();
+
+ if (!token) {
+ throw new Error("请先登录");
+ }
+
+ const controller = new AbortController();
+
+ const fetchPromise = fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ "X-Request-ID": requestId,
+ },
+ body: JSON.stringify({
+ query: params.prompt,
+ user_id: params.userId,
+ user_name: params.userName,
+ user_token: params.user_token || "123",
+ user_role: "student",
+ conversation_id: params.conversationId || null,
+ }),
+ signal: controller.signal,
+ }).then((response) => {
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ if (!response.body) {
+ throw new Error("Response body is null");
+ }
+ return {
+ reader: response.body.getReader(),
+ decoder: new TextDecoder("utf-8"),
+ };
+ });
+
+ return {
+ stream: fetchPromise,
+ cancel: (reason) => {
+ if (!controller.signal.aborted) {
+ controller.abort(reason);
+ }
+ },
+ };
+}
+
+/**
+ * 处理流式响应
+ * @param {ReadableStreamDefaultReader} reader 读取器
+ * @param {TextDecoder} decoder 文本解码器
+ * @param {Function} onMessage 消息回调
+ * @param {Function} onError 错误回调
+ * @param {Function} onComplete 完成回调
+ */
+export async function processStream(
+ reader,
+ decoder,
+ { onMessage, onError, onComplete }
+) {
+ let buffer = "";
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split("\n");
+ buffer = lines.pop() || "";
+
+ for (const line of lines) {
+ if (!line.trim()) continue;
+
+ try {
+ const data = JSON.parse(line);
+ if (typeof onMessage === "function") {
+ onMessage(data);
+ }
+ } catch (e) {
+ console.warn("解析消息失败:", line, e);
+ }
+ }
+ }
+
+ if (typeof onComplete === "function") {
+ onComplete();
+ }
+ } catch (error) {
+ if (error.name !== "AbortError" && typeof onError === "function") {
+ onError(error);
+ }
+ } finally {
+ reader.releaseLock();
+ }
+}
diff --git a/src/utils/toast.js b/src/utils/toast.js
new file mode 100644
index 0000000..2b45e22
--- /dev/null
+++ b/src/utils/toast.js
@@ -0,0 +1,48 @@
+import { Message } from 'element-ui'
+
+/**
+ * 显示Toast消息
+ * @param {string} message - 消息内容
+ * @param {string} type - 消息类型: 'success', 'warning', 'info', 'error'
+ * @param {number} duration - 显示时长,默认3000ms
+ */
+export function showToast(message, type = 'info', duration = 3000) {
+ Message({
+ message,
+ type,
+ duration,
+ showClose: true
+ })
+}
+
+/**
+ * 显示成功消息
+ * @param {string} message - 消息内容
+ */
+export function showSuccess(message) {
+ showToast(message, 'success')
+}
+
+/**
+ * 显示错误消息
+ * @param {string} message - 消息内容
+ */
+export function showError(message) {
+ showToast(message, 'error')
+}
+
+/**
+ * 显示警告消息
+ * @param {string} message - 消息内容
+ */
+export function showWarning(message) {
+ showToast(message, 'warning')
+}
+
+/**
+ * 显示信息消息
+ * @param {string} message - 消息内容
+ */
+export function showInfo(message) {
+ showToast(message, 'info')
+}
\ No newline at end of file