diff --git a/common/http.api.js b/common/http.api.js index f54f97d..30a4823 100644 --- a/common/http.api.js +++ b/common/http.api.js @@ -236,18 +236,21 @@ const install = (Vue, vm) => { // 获取消息接收方用户信息 let GetReceiverUserInfoApi = (params = {}) => vm.$u.get("api/Dialogue/GetReceiverUserInfo", params); - // 置顶一个会话 - let OverheadOneDialogueApi = (params = {}) => - vm.$u.post("api/Dialogue/OverheadOneDialogue", params); - // 将会话消息标记为已读 - let ReadMessageApi = (params = {}) => - vm.$u.post("api/Dialogue/ReadMessage", params); + // 置顶一个会话 + let OverheadOneDialogueApi = (params = {}) => + vm.$u.post("api/Dialogue/OverheadOneDialogue", params); + // 将会话消息标记为已读 + let ReadMessageApi = (params = {}) => + vm.$u.post("api/Dialogue/ReadMessage", params); // 删除会话 let DeleteDialogueApi = (params = {}) => vm.$u.post("api/Dialogue/DeleteDialogue", params); + // 转人工服务 + let TransferToALiveAgentApi = (params = {}) => + vm.$u.post("api/Dialogue/TransferToALiveAgent", params); // 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下 - vm.$u.api = { + vm.$u.api = { UploadSingleImage, getTeacherInfo, getData, @@ -306,16 +309,17 @@ const install = (Vue, vm) => { UpdateUserApi, GetTeacherListApi, GetDialogueListApi, - GetDialogueList_UserApi, - AddDialogueApi, - SendMessage_PrivateApi, - GetChatHistoryDataApi, - GetReceiverUserInfoApi, - OverheadOneDialogueApi, - ReadMessageApi, - DeleteDialogueApi, - }; -}; + GetDialogueList_UserApi, + AddDialogueApi, + SendMessage_PrivateApi, + GetChatHistoryDataApi, + GetReceiverUserInfoApi, + OverheadOneDialogueApi, + ReadMessageApi, + DeleteDialogueApi, + TransferToALiveAgentApi, + }; +}; export default { install, diff --git a/pages/chat/index.vue b/pages/chat/index.vue index 1a4ce7c..f2ed716 100644 --- a/pages/chat/index.vue +++ b/pages/chat/index.vue @@ -374,13 +374,6 @@ export default { }); }, - // 格式化时间 - formatTime(date) { - const hours = date.getHours().toString().padStart(2, "0"); - const minutes = date.getMinutes().toString().padStart(2, "0"); - return `${hours}:${minutes}`; - }, - // 是否显示时间 isShowTime(index) { return shouldShowTime(this.vuex_msgList, index); diff --git a/pages/home/history/index.vue b/pages/home/history/index.vue index bf888ea..457e1b1 100644 --- a/pages/home/history/index.vue +++ b/pages/home/history/index.vue @@ -52,7 +52,7 @@ {{ group.id }} - + - + @@ -164,6 +167,7 @@ export default { chatHistoryAI: [], // ai咨询历史记录 chatHistoryTeacher: [], // 人工咨询历史记录 openedItems: {}, // 记录每个项目的滑动状态 + openedSwipeId: "", // 当前展开的侧滑项 id }; }, computed: { @@ -207,16 +211,9 @@ export default { // 重置滑动状态 closeAllSwipeActions() { - const swipeActionRefs = this.$refs.swipeActionRef || []; - const refs = Array.isArray(swipeActionRefs) - ? swipeActionRefs - : [swipeActionRefs]; - - refs.forEach((ref) => { - if (ref && typeof ref.closeAll === "function") { - ref.closeAll(); - } - }); + const ids = this.currentHistoryItemIds || []; + ids.forEach((id) => this.closeSwipeActionById(id)); + this.openedSwipeId = ""; }, // 清空 openedItems 并调用 closeAllSwipeActions() @@ -225,6 +222,16 @@ export default { this.closeAllSwipeActions(); }, + closeSwipeActionById(itemId) { + if (!itemId) return; + const swipeRef = this.$refs[`swipe-${itemId}`]; + const ref = Array.isArray(swipeRef) ? swipeRef[0] : swipeRef; + if (ref && typeof ref.closeAll === "function") { + ref.closeAll(); + } + this.$set(this.openedItems, itemId, "none"); + }, + // 获取ai历史记录 async getChatHistoryList() { this.$u.api.GetConversationPage().then((res) => { @@ -465,6 +472,16 @@ export default { // 处理滑动操作变化 handleSwipeChange(e, itemId) { this.$set(this.openedItems, itemId, e); + if (e === "right" || e === "left") { + if (this.openedSwipeId && this.openedSwipeId !== itemId) { + this.closeSwipeActionById(this.openedSwipeId); + } + this.openedSwipeId = itemId; + return; + } + if (e === "none" && this.openedSwipeId === itemId) { + this.openedSwipeId = ""; + } }, // 处理单个删除操作 diff --git a/pages/home/index/index.vue b/pages/home/index/index.vue index 0556eab..44b0b90 100644 --- a/pages/home/index/index.vue +++ b/pages/home/index/index.vue @@ -128,7 +128,7 @@ @@ -136,11 +136,77 @@ {{ message.displayTime }} + + + + + 请选择您要咨询的类型,以便系统为您转接对应的客服 + + + + + 招生咨询 + + + + 迎新咨询 + + + + + {{ getTransferCardTip(message) }} + + + @@ -157,7 +223,9 @@ { - // setTimeout(() => { - const query = uni.createSelectorQuery().in(this); - query - .select(".chat-content") - .boundingClientRect((data) => { - if (data) { - console.log("chat-content高度:", data.height); - - // 同时设置scrollTop和scrollToView - this.scrollTop = data.height + 200; - // if (this.messageGroups.length > 0) { - // const lastMessage = - // this.messageGroups[this.messageGroups.length - 1]; - // this.scrollToView = "msg-" + lastMessage.id; - // console.log("设置scrollToView:", this.scrollToView); - // } - } - }) - .exec(); - // }, 300); + scrollToBottomByContentHeight(this, { + selector: ".chat-content", + extraOffset: 200, }); }, handleFeatureClick(item) { @@ -580,6 +615,11 @@ export default { return; } + if (item.title === "转人工") { + this.handleTransferEntryClick(); + return; + } + if (item.title === "电话咨询") { this.advicePhoneShow = true; return; @@ -598,6 +638,153 @@ export default { } }, + createLocalUserTextMessage(messageText) { + return { + id: Math.random().toString(36).substring(2, 15), + message: messageText, + sendDate: "", + isSend: true, + isRead: false, + interactMode: 0, + messageType: 0, + timeLabel: 0, + displayTime: "", + }; + }, + + createTransferCardMessage() { + return { + id: "transfer_" + Math.random().toString(36).substring(2, 15), + message: "", + sendDate: "", + isSend: true, + isRead: false, + interactMode: 1, + messageType: 0, + timeLabel: 0, + displayTime: "", + customType: "transferCard", + transferStatus: "idle", + selectedConsultationType: null, + transferTipText: "", + }; + }, + + handleTransferEntryClick() { + if (!this.isChat) { + this.resetChatState(); + } + + this.isLoadingMore = false; + this.isTransferSubmitting = false; + + const nextList = this.aiMessageGroups.filter( + (m) => !(m && m.customType === "transferCard") + ); + + nextList.push(this.createLocalUserTextMessage("转人工")); + const card = this.createTransferCardMessage(); + this.activeTransferCardId = card.id; + nextList.push(card); + this.$store.commit("push_AiMsgList", nextList); + }, + + getTransferCardTip(message) { + if (!message) return ""; + if (this.isTransferSubmitting) return "正在为您转接人工客服,请稍等..."; + if (message.transferTipText) return message.transferTipText; + return ""; + }, + + handleTransferTypeSelect(consultationType) { + const cardId = this.activeTransferCardId; + const findCardIndex = (list) => { + if (cardId) { + const byId = list.findIndex( + (m) => m && m.id === cardId + ); + if (byId >= 0) return byId; + } + return list.findIndex( + (m) => m && m.customType === "transferCard" + ); + }; + const list = this.aiMessageGroups; + const idx = findCardIndex(list); + if (idx < 0) return; + + const current = list[idx] || {}; + if (this.isTransferSubmitting || current.transferStatus === "done") return; + + const next = { + ...current, + transferStatus: "idle", + selectedConsultationType: consultationType, + transferTipText: "", + }; + const nextList = list.slice(); + nextList.splice(idx, 1, next); + this.$store.commit("push_AiMsgList", nextList); + this.isTransferSubmitting = true; + + const params = { + consultationType, + message: "转人工", + ip: "", + }; + + if (this.currentDMid) { + params.dialogueManagementId = this.currentDMid; + } + + this.$u.api + .TransferToALiveAgentApi(params) + .then((res) => { + this.isTransferSubmitting = false; + const latestList = this.aiMessageGroups; + const nextIdx = findCardIndex(latestList); + if (nextIdx < 0) return; + const succeed = res && res.succeed !== false; + const serverTip = + (res && res.data && res.data.message) || + (res && res.error) || + ""; + if (!succeed) { + const updated = { + ...latestList[nextIdx], + transferStatus: "idle", + transferTipText: serverTip, + }; + const updatedList = latestList.slice(); + updatedList.splice(nextIdx, 1, updated); + this.$store.commit("push_AiMsgList", updatedList); + return; + } + const updated = { + ...latestList[nextIdx], + transferStatus: "done", + transferTipText: serverTip, + }; + const updatedList = latestList.slice(); + updatedList.splice(nextIdx, 1, updated); + this.$store.commit("push_AiMsgList", updatedList); + }) + .catch(() => { + this.isTransferSubmitting = false; + const latestList = this.aiMessageGroups; + const nextIdx = findCardIndex(latestList); + if (nextIdx < 0) return; + const updated = { + ...latestList[nextIdx], + transferStatus: "idle", + transferTipText: "转人工失败,请稍后重试", + }; + const updatedList = latestList.slice(); + updatedList.splice(nextIdx, 1, updated); + this.$store.commit("push_AiMsgList", updatedList); + }); + }, + // 修改发送消息的方法 sendMessageFn() { if (!this.messageValue) { @@ -624,7 +811,7 @@ export default { }; // 添加到消息列表 - this.messageGroups.push(userMessage); + this.$store.commit("push_AiMsg", userMessage); this.messageValue = ""; // 立即添加一个AI回复的加载状态消息 @@ -642,7 +829,7 @@ export default { }; // 添加加载状态消息到列表 - this.messageGroups.push(loadingMessage); + this.$store.commit("push_AiMsg", loadingMessage); const params = { query: sendMessage, @@ -663,7 +850,7 @@ export default { this.currentDMid = data.dmid; // 从消息列表中移除加载状态消息 - this.messageGroups = this.messageGroups.filter( + const baseList = this.aiMessageGroups.filter( (msg) => !msg.isLoading ); @@ -683,13 +870,13 @@ export default { }; // 添加到消息列表 - this.messageGroups.push(aiMessage); + this.$store.commit("push_AiMsgList", baseList.concat([aiMessage])); }) .catch((error) => { console.error("API请求失败:", error); // 从消息列表中移除加载状态消息 - this.messageGroups = this.messageGroups.filter( + const baseList = this.aiMessageGroups.filter( (msg) => !msg.isLoading ); @@ -706,7 +893,10 @@ export default { displayTime: "", }; - this.messageGroups.push(errorMessage); + this.$store.commit( + "push_AiMsgList", + baseList.concat([errorMessage]) + ); }); }, @@ -737,7 +927,7 @@ export default { }; // 添加到消息列表 - this.messageGroups.push(userMessage); + this.$store.commit("push_AiMsg", userMessage); // 立即添加一个AI回复的加载状态消息 const loadingMessage = { @@ -754,7 +944,7 @@ export default { }; // 添加加载状态消息到列表 - this.messageGroups.push(loadingMessage); + this.$store.commit("push_AiMsg", loadingMessage); const params = { id: item.id, @@ -772,7 +962,7 @@ export default { const data = res.data.entityInfo; // 从消息列表中移除加载状态消息 - this.messageGroups = this.messageGroups.filter( + const baseList = this.aiMessageGroups.filter( (msg) => !msg.isLoading ); @@ -792,11 +982,15 @@ export default { }; // 添加到消息列表 - this.messageGroups.push(aiMessage); + this.$store.commit( + "push_AiMsgList", + baseList.concat([aiMessage]) + ); } else { // 从消息列表中移除加载状态消息 - this.messageGroups = this.messageGroups.filter( - (msg) => !msg.isLoading + this.$store.commit( + "push_AiMsgList", + this.aiMessageGroups.filter((msg) => !msg.isLoading) ); } }) @@ -804,7 +998,7 @@ export default { console.error("API请求失败:", error); // 从消息列表中移除加载状态消息 - this.messageGroups = this.messageGroups.filter( + const baseList = this.aiMessageGroups.filter( (msg) => !msg.isLoading ); @@ -821,17 +1015,13 @@ export default { displayTime: "", }; - this.messageGroups.push(errorMessage); + this.$store.commit( + "push_AiMsgList", + baseList.concat([errorMessage]) + ); }); }, - // 格式化时间的辅助方法 - formatTime(date) { - const hours = date.getHours().toString().padStart(2, "0"); - const minutes = date.getMinutes().toString().padStart(2, "0"); - return `${hours}:${minutes}`; - }, - // 处理选中的对话 handleSelectConversation(data) { // 关闭弹窗 @@ -848,7 +1038,7 @@ export default { this.isSwitchingConversation = true; // 重置消息列表和分页相关状态 - this.messageGroups = []; + this.$store.commit("push_AiMsgList", []); this.pageQuery.PageIndex = 1; this.isLoadingMore = false; this.noMoreData = false; @@ -859,52 +1049,12 @@ export default { // 处理消息内容,如果是JSON格式则解析并格式化 processMessageContent(message) { - try { - // 尝试解析 JSON - const parsed = JSON.parse(message); - - // 如果是对象且包含特定字段,进行格式化 - if (typeof parsed === "object" && parsed !== null) { - let content = ""; - - // 提取字段 - const { answerTypeLabel, imageUrl, nearbyPaths } = parsed; - - // 拼接内容,如果有值则添加并换行 - if (answerTypeLabel) content += `${answerTypeLabel}\n`; - if (imageUrl) content += `${imageUrl}\n`; - if (nearbyPaths) content += `${nearbyPaths}\n`; - - // 如果提取到了内容,返回处理后的文本;否则返回原文本(避免误判) - // 去除末尾多余的换行符 - return content ? content.trim() : message; - } - } catch (e) { - // 解析失败,说明不是JSON格式,直接返回原消息 - return message; - } - - return message; + return processChatMessageContent(message); }, // 公共排序:按时间升序;时间相同,用户消息(interactMode=0)在前 sortMessages(list = []) { - const processedList = (list || []).map((item) => { - // 对每条消息进行处理 - return { - ...item, - message: this.processMessageContent(item.message), - }; - }); - - // console.log("processedList", processedList); - - return processedList.sort((a, b) => { - const timeA = new Date(a.sendDate).getTime(); - const timeB = new Date(b.sendDate).getTime(); - if (timeA === timeB) return a.interactMode - b.interactMode; - return timeA - timeB; - }); + return sortChatMessages(list); }, // 刷新当前对话的消息详情 @@ -922,7 +1072,10 @@ export default { const currentList = res.item2 || []; // 将消息按sendDate升序排列,时间相同时用户消息(interactMode=0)排在前面 - this.messageGroups = this.sortMessages(currentList); + this.$store.commit( + "push_AiMsgList", + this.sortMessages(currentList) + ); }) .finally(() => { // 延迟重置切换对话标志位,确保滚动事件处理完成 @@ -957,11 +1110,17 @@ export default { }) .then((res3) => { const prevList = res3?.item2 || []; - this.messageGroups = this.sortMessages(prevList); + this.$store.commit( + "push_AiMsgList", + this.sortMessages(prevList) + ); this.pageQuery.PageIndex = prevIndex; }); } else { - this.messageGroups = this.sortMessages(currentList); + this.$store.commit( + "push_AiMsgList", + this.sortMessages(currentList) + ); } }); }, @@ -1020,23 +1179,12 @@ export default { } // 将消息按sendDate升序排列,时间相同时用户消息(interactMode=0)排在前面 - const nextPageMessageGroups = res.item2.sort((a, b) => { - const timeA = new Date(a.sendDate).getTime(); - const timeB = new Date(b.sendDate).getTime(); + const nextPageMessageGroups = sortChatMessages(res.item2 || []); - // 如果时间相同,按interactMode排序(0排在前,1排在后) - if (timeA === timeB) { - return a.interactMode - b.interactMode; - } - - // 否则按时间升序排列 - return timeA - timeB; - }); - - this.messageGroups = [ - ...nextPageMessageGroups, - ...this.messageGroups, - ]; + this.$store.commit( + "prepend_AiMsgList", + nextPageMessageGroups + ); }) .finally(() => { setTimeout(() => { @@ -1521,6 +1669,79 @@ export default { text-align: left; } } + + .message-transfer { + width: 100%; + margin-bottom: 40rpx; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + } + + .transfer-card { + width: 100%; + background-color: #ffffff; + border-radius: 16rpx; + padding: 40rpx 36rpx; + box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.06); + } + + .transfer-card-title { + padding: 0 16rpx; + font-family: PingFang SC; + font-size: 32rpx; + color: #333333; + line-height: 1.6; + font-weight: 500; + } + + .transfer-card-options { + margin-top: 32rpx; + // padding: 24rpx; + border-radius: 16rpx; + // background-color: #f6f8ff; + display: flex; + gap: 36rpx; + } + + .transfer-option { + flex: 1; + height: 112rpx; + border-radius: 16rpx; + background-color: #f6f8fa; + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + } + + .transfer-option.selected { + background-color: #eef0fe; + border-color: #dbe3ff; + } + + .transfer-option.disabled { + opacity: 0.7; + } + + .transfer-option-text { + font-family: PingFang SC; + font-size: 30rpx; + color: #333333; + font-weight: 500; + } + + .transfer-option.selected .transfer-option-text { + color: #4a6cf7; + } + + .transfer-card-status { + margin-top: 24rpx; + text-align: center; + font-size: 28rpx; + color: #999; + } } .chat-footer { diff --git a/store/index.js b/store/index.js index 60ed024..03972e6 100644 --- a/store/index.js +++ b/store/index.js @@ -43,6 +43,8 @@ const store = new Vuex.Store({ vuex_token: lifeData.vuex_token ? lifeData.vuex_token : "", // 如果vuex_version无需保存到本地永久存储,无需lifeData.vuex_version方式 vuex_version: "1.0.0", + // 智能客服消息组列表 + vuex_aiMessageGroups: [], // 当前聊天窗口消息列表(数组 vuex_msgList: [], // 最近联系人/会话列表 @@ -179,6 +181,42 @@ const store = new Vuex.Store({ if (!msg) return; state.vuex_msgList.unshift(msg); }, + + // ===== 智能客服:消息组列表维护(参照 vuex_msgList 维护方式) ===== + // 覆盖整个列表 + push_AiMsgList(state, list) { + state.vuex_aiMessageGroups = Array.isArray(list) ? list : []; + }, + // 追加历史消息到头部(分页加载) + prepend_AiMsgList(state, list) { + const prev = state.vuex_aiMessageGroups || []; + const next = Array.isArray(list) ? list : []; + const merged = [...next, ...prev]; + + const seen = new Set(); + state.vuex_aiMessageGroups = merged.filter((item) => { + const id = item && item.id; + if (!id) return true; + if (seen.has(id)) return false; + seen.add(id); + return true; + }); + }, + // 推送一条新消息(去重) + push_AiMsg(state, msg) { + if (!msg || !msg.id) return; + const exists = (state.vuex_aiMessageGroups || []).some( + (item) => item && item.id === msg.id + ); + if (!exists) { + state.vuex_aiMessageGroups.push(msg); + } + }, + // 插入一条消息到头部 + unshift_AiMsg(state, msg) { + if (!msg) return; + state.vuex_aiMessageGroups.unshift(msg); + }, // 更新置顶状态(本地) update_TopState(state, { friendId, isTop }) { state.vuex_userMsgList = (state.vuex_userMsgList || []).map((item) => { diff --git a/utils/chat.js b/utils/chat.js index 714a464..38c146d 100644 --- a/utils/chat.js +++ b/utils/chat.js @@ -123,3 +123,67 @@ export function scrollToBottomByContentHeight( .exec(); }); } + +/** + * 处理聊天消息内容:若内容为 JSON 字符串且符合预期结构,则提取并拼接为可读文本。 + * + * 说明: + * - 首页对话存在部分消息内容为 JSON 字符串的情况(如包含 `answerTypeLabel/imageUrl/nearbyPaths`) + * - 这里做“尽量解析、解析失败回退原文”的处理,避免误伤普通文本消息 + * + * @param {string} message - 原始消息文本 + * @returns {string} - 处理后的文本 + */ +export function processChatMessageContent(message) { + try { + const parsed = JSON.parse(message); + if (typeof parsed === "object" && parsed !== null) { + let content = ""; + const { answerTypeLabel, imageUrl, nearbyPaths } = parsed; + + if (answerTypeLabel) content += `${answerTypeLabel}\n`; + if (imageUrl) content += `${imageUrl}\n`; + if (nearbyPaths) content += `${nearbyPaths}\n`; + + return content ? content.trim() : message; + } + } catch (e) { + return message; + } + + return message; +} + +/** + * 聊天消息排序:按 `sendDate` 升序;时间相同时,用户消息(`interactMode=0`)排在前面。 + * 同时会对每条消息的 `message` 字段进行 `processChatMessageContent` 处理。 + * + * @param {Array} list - 会话详情接口返回的消息列表 + * @returns {Array} - 处理并排序后的新数组 + */ +export function sortChatMessages(list = []) { + const processedList = (list || []).map((item) => ({ + ...item, + message: processChatMessageContent(item && item.message), + })); + + return processedList.sort((a, b) => { + const timeA = new Date(a && a.sendDate).getTime(); + const timeB = new Date(b && b.sendDate).getTime(); + if (timeA === timeB) return (a && a.interactMode) - (b && b.interactMode); + return timeA - timeB; + }); +} + +/** + * 将 Date 格式化为 `HH:mm`。 + * + * @param {Date} date - 时间对象 + * @returns {string} - 格式化字符串 + */ +export function formatHHmm(date) { + if (!(date instanceof Date) || Number.isNaN(date.getTime())) return ""; + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + return `${hours}:${minutes}`; +}