diff --git a/App.vue b/App.vue index c94eace..abef9c0 100644 --- a/App.vue +++ b/App.vue @@ -10,7 +10,7 @@ export default { // WebSocket 实例与连接控制 ws: null, // 当前 WebSocket 连接 lockReconnect: false, // 是否处于稳定连接,防止重复重连 - timeout: 30000, // 心跳间隔(毫秒) + // timeout: 30000, // 心跳间隔(毫秒) timeoutObj: null, // 心跳倒计时定时器 serverTimeoutObj: null, // 心跳响应等待定时器 timeoutnum: null, // 重连延时定时器 @@ -84,8 +84,6 @@ export default { }, // 初始化原生 WebSocket 连接 initWebSocket() { - console.log(this.buildWsUrl()); - try { this.ws = new WebSocket(this.buildWsUrl()); this.ws.onopen = () => this.handleWsOpen(); @@ -114,32 +112,29 @@ export default { // 启动心跳与超时处理 start() { this.timeoutObj = setTimeout(() => { - try { - if ( - this.ws && - this.ws.readyState === 1 && - this.vuex_user && - (this.vuex_user.id || this.vuex_user.Id) - ) { - this.ws.send("heartCheck"); - } else { - this.lockReconnect = false; - this.reconnect(); - } - } catch (err) { + //这里发送一个心跳,后端收到后,返回一个心跳消息 + if ( + this.ws && + this.ws.readyState === 1 && + this.vuex_user && + (this.vuex_user.Id || this.vuex_user.id) + ) { + //如果连接正常 + this.ws.send("heartCheck"); + } else { + //否则重连 this.lockReconnect = false; this.reconnect(); } // 心跳发送后等待后端响应,超时则断开重连 this.serverTimeoutObj = setTimeout(() => { - try { - this.ws && this.ws.close(); - } catch (e) {} + // console.log("[WebSocket] 心跳响应超时,断开重连"); + this.ws && this.ws.close(); this.lockReconnect = false; this.reconnect(); - }, this.timeout); - }, this.timeout); + }, 30000); + }, 30000); }, // 连接成功 handleWsOpen() { @@ -153,10 +148,12 @@ export default { // 收到任何消息都重置心跳 this.reset(); + console.log("[WebSocket] 收到消息:", e); + // 心跳消息不处理 - if (typeof e.data === "string" && e.data.indexOf("heartCheck") >= 0) { - return; - } + // if (typeof e.data === "string" && e.data.indexOf("heartCheck") >= 0) { + // return; + // } // 尝试解析为 JSON(与 oa-web-phone 保持一致的结构:{ Type, Data }) let msgData = null; @@ -169,6 +166,10 @@ export default { const type = msgData.Type; + console.log("收到消息类型:", type); + // 收到服务端心跳响应不处理 + if (type === "pong") return; + // 退出登录通知 if (type === "Exit") { try { @@ -209,14 +210,13 @@ export default { }, // 连接关闭 handleWsClose(e) { - console.log(`[WebSocket] 连接关闭: code=${e.code}, reason=${e.reason}`); + // console.log(`[WebSocket] 连接关闭: code=${e.code}, reason=${e.reason}`); this.lockReconnect = false; this.reconnect(); }, // 连接错误 handleWsError(e) { - console.log("[WebSocket] 连接错误:", e); - + // console.log("[WebSocket] 连接错误:", e); this.lockReconnect = false; this.reconnect(); }, @@ -237,7 +237,7 @@ export default { }, mounted() { // 使用与 oa-web-phone 相同的原生 WebSocket 通信方式 - // this.startLink(); // 现在一直断线重连,先注释 + this.startLink(); // 现在一直断线重连,先注释 }, }; diff --git a/common/http.api.js b/common/http.api.js index 01596bd..c51c910 100644 --- a/common/http.api.js +++ b/common/http.api.js @@ -218,9 +218,12 @@ const install = (Vue, vm) => { // 获取教师列表(学生端-招办在线) let GetTeacherListApi = (params = {}) => vm.$u.get("api/Dialogue/GetTeacherList", params); - // 获取会话列表 + // 获取会话列表-教师 let GetDialogueListApi = (params = {}) => vm.$u.get("api/Dialogue/GetDialogueList", params); + // 获取会话列表-用户 + let GetDialogueList_UserApi = (params = {}) => + vm.$u.get("api/Dialogue/GetDialogueList_User", params); // 创建会话 let AddDialogueApi = (params = {}) => vm.$u.post("api/Dialogue/AddDialogue", params); @@ -236,6 +239,9 @@ const install = (Vue, vm) => { // 置顶一个会话 let OverheadOneDialogueApi = (params = {}) => vm.$u.post("api/Dialogue/OverheadOneDialogue", params); + // 删除会话 + let DeleteDialogueApi = (params = {}) => + vm.$u.post("api/Dialogue/DeleteDialogue", params); // 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下 vm.$u.api = { @@ -297,6 +303,7 @@ const install = (Vue, vm) => { UpdateUserApi, GetTeacherListApi, GetDialogueListApi, + GetDialogueList_UserApi, AddDialogueApi, SendMessage_PrivateApi, GetChatHistoryDataApi, diff --git a/components/ChatHistory.vue b/components/ChatHistory.vue index 5ec355f..c81f474 100644 --- a/components/ChatHistory.vue +++ b/components/ChatHistory.vue @@ -44,7 +44,7 @@ > @@ -63,7 +63,7 @@ 'chat-item-active': item.isActiveChat, }" @click.stop=" - selectChatItem(groupIndex, index, item.id, item.conversationId) + selectChatItem(item.id, item.conversationId, item.receiverId) " > @@ -90,12 +90,7 @@ [], + }, + chatHistoryTeacher: { type: Array, default: () => [], }, @@ -172,8 +171,6 @@ export default { return { baseUrl: "", showPopup: false, - currentActiveGroup: -1, - currentActiveIndex: -1, activeItemId: "", // 存储当前激活项的ID scrollToView: "", // 用于scroll-into-view属性 tabList: [ @@ -193,6 +190,12 @@ export default { }, computed: { + currentChatHistory() { + return this.currentTab === 0 + ? this.chatHistoryAI + : this.chatHistoryTeacher; + }, + headSculptureUrl() { if (this.vuex_user.HeadSculptureUrl) { return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl; @@ -224,7 +227,7 @@ export default { } }, // 监听聊天历史数据变化,找到激活项并滚动 - chatHistoryList3: { + chatHistoryAI: { handler() { this.$nextTick(() => { this.scrollToActiveItem(); @@ -308,10 +311,10 @@ export default { for ( let groupIndex = 0; - groupIndex < this.chatHistoryList3.length; + groupIndex < this.chatHistoryAI.length; groupIndex++ ) { - const group = this.chatHistoryList3[groupIndex]; + const group = this.chatHistoryAI[groupIndex]; for (let index = 0; index < group.conversation.length; index++) { const item = group.conversation[index]; if (item.isActiveChat) { @@ -326,16 +329,21 @@ export default { } }, - selectChatItem(groupIndex, index, id, conversationId) { - // this.currentActiveGroup = groupIndex; - // this.currentActiveIndex = index; - console.log("selectChatItem", groupIndex, index, id, conversationId); - - // 向父组件发送选中的对话信息 - this.$emit("select-conversation", { - id, - conversationId, - }); + selectChatItem(id, conversationId = null, receiverId = null) { + if (this.currentTab === 0) { + // 点击AI聊天项 + // 向父组件发送选中的对话信息 + this.$emit("select-conversation", { + id, + conversationId, + }); + } else { + // 点击教师聊天项 + this.$store.dispatch("selectTeacherChatItem", { + id, + receiverId, + }); + } }, handleCreateConversation() { diff --git a/pages/chat/index.vue b/pages/chat/index.vue index 1edfe4b..33e0711 100644 --- a/pages/chat/index.vue +++ b/pages/chat/index.vue @@ -2,7 +2,7 @@ @@ -15,6 +15,30 @@ :scroll-into-view="scrollToView" scroll-with-animation > + +
+ +
+
{{ vuex_msgUser.name }}
+ +
+ + {{ vuex_msgUser.collegeName }} +
+ +
+ + {{ vuex_msgUser.collegeName }} +
+
+
+ @@ -44,12 +68,12 @@ @@ -155,6 +179,7 @@ export default { onLoad(options) { console.log(this.vuex_msgList); + console.log(this.vuex_msgUser, "this.vuex_msgUser"); this.baseUrl = this.$u.http.config.baseUrl; @@ -168,6 +193,14 @@ export default { }, computed: { + receiverHeadSculptureUrl() { + if (this.vuex_msgUser.headSculptureUrl) { + return this.baseUrl + "/" + this.vuex_msgUser.headSculptureUrl; + } + + return "/static/common/images/avatar_default2.png"; + }, + headSculptureUrl() { if (this.vuex_user.HeadSculptureUrl) { return this.baseUrl + "/" + this.vuex_user.HeadSculptureUrl; @@ -231,7 +264,7 @@ export default { message: this.messageValue, messageType: 0, filePath: "", - interactMode: 0, + // interactMode: 0, }; this.$store.commit("push_Msg", msgUserData); @@ -274,17 +307,7 @@ export default { }) .then((res) => { const msgList = res.data.item1.reverse(); - // 注:现在的消息返回值缺少 interactMode 字段,先手动mock - for (var i = 0; i < msgList.length; i++) { - msgList[i].interactMode = msgList[i].interactMode || 0; - - // if (this.PageIndex != 1) { - // this.$store.commit("unshift_Msg", msgList[i]); - // } else { - // this.$store.commit("push_Msg", msgList[i]); - // } - } this.$store.commit("push_MsgList", msgList); }); }, @@ -399,6 +422,62 @@ export default { // padding: 10rpx 0; // } + .teacher-info-card { + background-color: #ffffff; + padding: 30rpx; + border-radius: 16rpx; + margin: 30rpx 0; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); + + display: flex; + align-items: center; + margin-bottom: 36rpx; + + .teacher-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 10rpx; + margin-right: 30rpx; + object-fit: cover; + } + + .teacher-info { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 8rpx; + flex: 1; + + .teacher-name { + font-family: PingFang SC; + font-weight: bold; + font-size: 32rpx; + color: #333333; + } + + .teacher-school, + .teacher-college { + display: flex; + align-items: center; + + .school-icon, + .college-icon { + margin-right: 16rpx; + width: 24rpx; + height: 24rpx; + display: inline-block; + text-align: center; + } + + .school-text, + .college-text { + font-size: 24rpx; + color: #666666; + } + } + } + } + .message-time { text-align: center; font-size: 24rpx; diff --git a/pages/home/admissions/index.vue b/pages/home/admissions/index.vue index e129c48..33ab4bb 100644 --- a/pages/home/admissions/index.vue +++ b/pages/home/admissions/index.vue @@ -18,31 +18,39 @@ - - - - - - {{ teacher.name }} - - - 在线 + + + + + + + {{ teacher.name }} + + + 在线 + + {{ + teacher.collegeName + }} + + + 立即提问 - {{ teacher.collegeName }} - - - 立即提问 + + + + @@ -74,20 +82,20 @@ export default { { id: 1, name: "迎新在线" }, ], teacherList: [ - { - id: 1, - name: "孙老师", - department: "招就处", - avatar: "/static/common/images/avatar.png", - online: true, - }, - { - id: 2, - name: "杨老师", - department: "电子信息学院", - avatar: "/static/common/images/student.png", - online: false, - }, + // { + // id: 1, + // name: "孙老师", + // department: "招就处", + // avatar: "/static/common/images/avatar.png", + // online: true, + // }, + // { + // id: 2, + // name: "杨老师", + // department: "电子信息学院", + // avatar: "/static/common/images/student.png", + // online: false, + // }, ], }; }, @@ -106,14 +114,8 @@ export default { handleAskQuestion(teacher) { console.log("点击咨询:", teacher); - var msgUserData = { - avatar: teacher.headSculptureUrl, - friendId: teacher.id, - friendName: teacher.name, - }; - - // 进入对话会话 - this.$store.dispatch("openOrCreateDialogue", msgUserData); + // 点击立即提问,进入对话会话 + this.$store.dispatch("createDialogue", teacher); return; diff --git a/pages/home/index/index.vue b/pages/home/index/index.vue index c32cd18..783d8c6 100644 --- a/pages/home/index/index.vue +++ b/pages/home/index/index.vue @@ -259,7 +259,8 @@ { - this.chatHistoryList3 = res.data; - if (this.chatHistoryList3.length > 0) { - this.chatHistoryList3 = res.data.map((group) => { + // 获取人工咨询历史记录 + async GetDialogueList_User() { + this.$u.api.GetDialogueList_UserApi().then((res) => { + this.chatHistoryTeacher = res.data; + if (this.chatHistoryTeacher.length > 0) { + this.chatHistoryTeacher = res.data.map((group) => { // 对每个组的conversation数组进行倒序排序 return { ...group, @@ -511,7 +515,29 @@ export default { }; }); } - console.log("this.chatHistoryList3", this.chatHistoryList3); + }); + }, + + // 获取ai历史记录 + async getChatHistoryList() { + this.$u.api.GetConversationPage().then((res) => { + this.chatHistoryAI = res.data; + if (this.chatHistoryAI.length > 0) { + this.chatHistoryAI = res.data.map((group) => { + // 对每个组的conversation数组进行倒序排序 + return { + ...group, + conversation: group.conversation.sort((a, b) => { + // 将日期字符串转换为时间戳并比较(倒序) + return ( + new Date(b.startTime).getTime() - + new Date(a.startTime).getTime() + ); + }), + }; + }); + } + console.log("this.chatHistoryAI", this.chatHistoryAI); }); }, diff --git a/static/common/images/avatar_default2.png b/static/common/images/avatar_default2.png new file mode 100644 index 0000000..b3db991 Binary files /dev/null and b/static/common/images/avatar_default2.png differ diff --git a/store/index.js b/store/index.js index fd726f0..75f9025 100644 --- a/store/index.js +++ b/store/index.js @@ -94,8 +94,6 @@ const store = new Vuex.Store({ // text: "我的" // } ], - vuex_education: [], - vuex_schoolName: "", }, mutations: { $uStore(state, payload) { @@ -237,7 +235,7 @@ const store = new Vuex.Store({ // 获取聊天记录(私聊) async fetchChatRecord( { commit, state }, - { userId, friendId, chatType = 0, PageIndex = 1, PageSize = 20 } + { userId, friendId, PageIndex = 1, PageSize = 20 } ) { const params = { userId, friendId, PageIndex, PageSize }; @@ -262,58 +260,73 @@ const store = new Vuex.Store({ } }, - // 统一入口:打开或创建会话 - // 使用位置:所有需要进入聊天的入口调用该方法,避免各页面重复逻辑 - // 1) 获取最新会话列表 - // 2) 查找是否存在 friendId 对应的会话 - // 3) 有则设置当前会话并(可选)跳转;无则创建,成功后递归重试 - async openOrCreateDialogue({ commit, state, dispatch }, user) { - const { friendId, friendName, avatar, chatType = 0 } = user || {}; - const attempt = - user && typeof user.attempt === "number" ? user.attempt : 1; - if (!friendId) return Promise.reject(new Error("缺少 friendId")); + // 点击聊天记录,切换到该会话 + selectTeacherChatItem({ commit, dispatch }, { id, receiverId }) { + // 清空消息列表,避免旧消息干扰 + commit("push_MsgList", []); + + Vue.prototype.$u.api + .GetReceiverUserInfoApi({ Id: receiverId }) + .then((res) => { + if (res.succeed && res.data) { + commit("set_MsgUser", { ...res.data, dialogueManagementId: id }); + uni.navigateTo({ + url: `/pages/chat/index`, + }); + return; + } + }); + }, + + // 点击立即提问进入会话 + // 1) 创建新会话 + // 2) 获取接收者信息 + // 3) 进入会话 + async createDialogue({ commit, state, dispatch }, user) { + const { id, dialogueManagementId } = user || {}; + if (!id) return Promise.reject(new Error("缺少 id")); // 清空消息列表,避免旧消息干扰 commit("push_MsgList", []); - // 第一步:获取列表(复用现有 action,保证 state 同步更新) - const list = await dispatch("getUserlist"); - - // 第二步:查找是否存在该会话(兼容后端字段 friendId/receiverId) - const target = (list || []).find( - (i) => i && (i.receiverId === friendId || i.friendId === friendId) - ); - if (target) { - commit("set_MsgUser", target); - - // 跳转到对话页,参数按需调整 + if ( + dialogueManagementId && + dialogueManagementId !== "00000000-0000-0000-0000-000000000000" + ) { + // 有会话ID,直接进入会话 + commit("set_MsgUser", { ...user }); uni.navigateTo({ url: `/pages/chat/index`, }); - - return target; + return; } - // 第二次调用时,无论成功与否均在第三步(创建)之前结束 - if (attempt >= 2) { - return false; - } - - // 第三步:不存在则创建,成功后重试本流程 - const res = await Vue.prototype.$u.api.AddDialogueApi({ - receiverId: friendId, - onlineConsultationType: 0, // 0:招生在线;1:迎新在线(可按业务传入) + // 没有会话id创建新会话 + const res1 = await Vue.prototype.$u.api.AddDialogueApi({ + receiverId: id, + onlineConsultationType: 1, // 创建在线咨询固定传1 }); - if (res && res.succeed === true) { - // 创建成功后,重新执行流程(再次获取列表并进入会话) - return dispatch("openOrCreateDialogue", { - friendId, - friendName, - avatar, - chatType, - attempt: attempt + 1, + + const resId = res1.data?.dialogueManagementId || ""; + + if (res1 && res1.succeed) { + // 获取接收者信息,这里没啥用(先注释) + // Vue.prototype.$u.api.GetReceiverUserInfoApi({ Id: id }).then((res) => { + // if (res.succeed && res.data) { + // commit("set_MsgUser", { ...res.data, dialogueManagementId }); + // uni.navigateTo({ + // url: `/pages/chat/index`, + // }); + // return; + // } + // }); + commit("set_MsgUser", { ...user, dialogueManagementId: resId }); + uni.navigateTo({ + url: `/pages/chat/index`, }); + return; } + return Promise.reject(new Error(res?.error || "创建会话失败")); }, }, diff --git a/types/global.d.ts b/types/global.d.ts index 42e5ec3..7094297 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -13,7 +13,7 @@ declare global { type __VLS_IsAny = 0 extends 1 & T ? true : false; type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; type __VLS_SpreadMerge = Omit & B; - type __VLS_WithComponent = + type __VLS_WithComponent = N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } : N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } : N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :