diff --git a/subPackages/chat/chat.vue b/subPackages/chat/chat.vue index 49af92d..eb336b6 100644 --- a/subPackages/chat/chat.vue +++ b/subPackages/chat/chat.vue @@ -45,11 +45,13 @@ @@ -594,6 +596,7 @@ export default { scrollIntoView: '', // 修复: 增加 scrollIntoView 变量 scrollViewHeight: 0, // 滚动视图高度 oldScrollViewHeight: 0, // 加载历史消息前的滚动高度 + scrollViewStyle: '', // 动态设置 scroll-view 高度样式 // 分页相关 page: 1, @@ -688,6 +691,9 @@ export default { if (typeof webSocketManager !== 'undefined') { webSocketManager.addMessageHandler(this.handleSocketMessage); } + + // 计算并设置 scroll-view 的明确高度 + this.calculateScrollViewHeight(); }, beforeDestroy() { @@ -698,6 +704,52 @@ export default { }, methods: { + /** + * 计算并设置 scroll-view 的明确高度 + * 根据 uni-app 文档,scroll-view 需要明确的高度才能正常滚动 + */ + calculateScrollViewHeight() { + this.$nextTick(() => { + const systemInfo = uni.getSystemInfoSync(); + const windowHeight = systemInfo.windowHeight; // 窗口高度(px) + + // 使用 uni.createSelectorQuery 获取头部和底部的高度 + const query = uni.createSelectorQuery().in(this); + + // 查询头部高度 + query.select('.chat-header').boundingClientRect((headerRect) => { + const headerHeight = headerRect ? headerRect.height : 0; + + // 查询底部高度 + const query2 = uni.createSelectorQuery().in(this); + query2.select('.chat-footer').boundingClientRect((footerRect) => { + const footerHeight = footerRect ? footerRect.height : 0; + + // 计算 scroll-view 的可用高度 + const scrollHeight = windowHeight - headerHeight - footerHeight; + + // 设置样式(使用 px 单位,因为系统信息返回的是 px) + this.scrollViewStyle = `height: ${scrollHeight}px;`; + + console.log('Scroll-view height calculated:', { + windowHeight, + headerHeight, + footerHeight, + scrollHeight + }); + }).exec(); + }).exec(); + }); + }, + + /** + * 滚动事件监听 + * 记录当前滚动位置 + */ + onScroll(e) { + this.scrollTop = e.detail.scrollTop; + }, + /** * 工具方法:移除 ID 前缀以便比较 * e.g. "doctor-123" -> "123" @@ -784,12 +836,14 @@ export default { /** * 获取当前消息列表的总高度 * 用于保持加载历史消息后的滚动位置 + * 在加载历史消息前调用,记录当前高度 */ getScrollHeight() { const query = uni.createSelectorQuery().in(this); query.select('.message-list-container').boundingClientRect(res => { if (res) { - this.oldScrollViewHeight = this.scrollViewHeight; + // 记录加载历史消息前的高度 + this.oldScrollViewHeight = res.height; this.scrollViewHeight = res.height; } }).exec(); @@ -804,6 +858,13 @@ export default { } }, + /** + * 监听新消息事件 + */ + /** + * 监听新消息事件 + * 优化:只在用户不在滚动时才自动滚动到底部 + */ /** * 监听新消息事件 */ @@ -815,26 +876,105 @@ export default { /** * 添加消息到列表并去重 + * 修复:优先检查临时消息,放宽时间戳匹配条件,改进内容匹配逻辑(处理 JSON 格式差异) */ addMessage(message) { - if (!this.messages.some(m => m.id === message.id)) { - // 数据标准化 - if (!message.message_content && message.content) { - message.message_content = message.content; - } - // 类型标准化 - if (message.message_type === undefined && message.type !== undefined) { - const messageTypeMap = { - text: 0, image: 1, audio: 2, video: 3, prescription: 4, file: 5, - register: 10, 'patient-experience': 11, 'product-card': 12, 'end-consultation': 13 - }; - message.message_type = messageTypeMap[message.type] || 0; - } - - console.log('push', message); - this.messages.push(message); - this.scrollToBottom(); + // 数据标准化 + if (!message.message_content && message.content) { + message.message_content = message.content; } + // 类型标准化 + if (message.message_type === undefined && message.type !== undefined) { + const messageTypeMap = { + text: 0, image: 1, audio: 2, video: 3, prescription: 4, file: 5, + register: 10, 'patient-experience': 11, 'product-card': 12, 'end-consultation': 13 + }; + message.message_type = messageTypeMap[message.type] || 0; + } + + // 检查是否已存在相同 ID 的消息 + const existingIndexById = this.messages.findIndex(m => m.id === message.id); + if (existingIndexById !== -1) { + // 如果 ID 相同,更新现有消息(服务器返回的消息替换临时消息) + this.messages[existingIndexById] = message; + console.log('update message by id', message.id); + return; + } + + // 辅助函数:标准化消息内容用于比较(处理 JSON 字符串和对象的差异) + const normalizeContent = (content) => { + if (!content) return ''; + if (typeof content === 'string') { + try { + // 尝试解析 JSON 字符串 + const parsed = JSON.parse(content); + return JSON.stringify(parsed); + } catch (e) { + // 不是 JSON,直接返回 + return content; + } + } else if (typeof content === 'object') { + return JSON.stringify(content); + } + return String(content); + }; + + // 优先检查临时消息(isTemporary: true) + // 如果当前消息是服务器返回的(没有 isTemporary 标记),查找临时消息并替换 + if (!message.isTemporary) { + const tempMessageIndex = this.messages.findIndex(m => { + // 查找临时消息,且发送者相同 + if (!m.isTemporary) return false; + const mSenderId = m.sender_user_id || ''; + const messageSenderId = message.sender_user_id || ''; + if (mSenderId !== messageSenderId) return false; + + // 比较消息内容(标准化后比较) + const mContent = normalizeContent(m.message_content || m.content || ''); + const messageContent = normalizeContent(message.message_content || message.content || ''); + if (mContent !== messageContent) return false; + + // 时间戳匹配(放宽到30秒,处理网络延迟) + const mTime = m.created_at || m.timestamp || 0; + const messageTime = message.created_at || message.timestamp || 0; + return Math.abs(mTime - messageTime) < 30000; // 30秒 + }); + + if (tempMessageIndex !== -1) { + // 找到临时消息,用服务器返回的消息替换 + this.messages[tempMessageIndex] = message; + console.log('replace temporary message with server message', message.id); + return; + } + } + + // 检查是否已存在相似消息(内容+发送者+时间戳匹配,允许30秒内的误差) + // 用于处理乐观更新和服务器返回消息的重复问题 + const messageContent = normalizeContent(message.message_content || message.content || ''); + const senderId = message.sender_user_id || ''; + const messageTime = message.created_at || message.timestamp || 0; + + const existingIndexByContent = this.messages.findIndex(m => { + const mContent = normalizeContent(m.message_content || m.content || ''); + const mSenderId = m.sender_user_id || ''; + const mTime = m.created_at || m.timestamp || 0; + + // 内容相同(标准化后)、发送者相同、时间差在30秒内 + return mContent === messageContent && + mSenderId === senderId && + Math.abs(mTime - messageTime) < 30000; // 放宽到30秒 + }); + + if (existingIndexByContent !== -1) { + // 如果找到相似消息,用服务器返回的消息替换临时消息(保留服务器 ID) + this.messages[existingIndexByContent] = message; + console.log('update message by content', message.id); + return; + } + + // 新消息,添加到列表 + console.log('push new message', message.id); + this.messages.push(message); }, toggleDoctorDetail() { @@ -930,6 +1070,7 @@ export default { const messageType = messageTypeMap[message.type] || 0; // 构造完整消息对象 (本地立即显示) + // 修复:标记为临时消息,用于后续去重 const fullMessage = { ...message, id: Date.now().toString(), @@ -940,16 +1081,15 @@ export default { created_at: now.getTime(), timestamp: now.getTime(), message_content: message.message_content || message.content || '', - message_type: messageType + message_type: messageType, + isTemporary: true // 标记为临时消息,服务器返回后会替换 }; // Fix: Optimistic UI update - Add message locally immediately this.addMessage(fullMessage); - // 存入本地 Store - if (typeof ChatManager !== 'undefined') { - ChatManager.addMessageToRoom(this.doctorUserInfo.room_id, fullMessage); - } + // 注意:不调用 ChatManager.addMessageToRoom,避免触发 new-room-message 事件导致消息重复 + // 服务器返回的消息会通过 WebSocket 触发 handleNewRoomMessage,由去重逻辑处理 // 构造请求数据 (发送给服务器) const requestData = { @@ -1131,29 +1271,57 @@ export default { } }, - // 修复: 采用 scroll-into-view 替代 scrollTop 计算,更稳健 + /** + * 滚动到底部 + * 优化:先清空 scroll-into-view,使用 $nextTick 和适当延时,再设置 scroll-into-view + * 根据文档,scroll-into-view 优先级高于 scroll-top,需要避免同时设置 + */ scrollToBottom() { + // 先清空 scroll-into-view,避免与 scroll-top 冲突 + // 注意:不清空 scrollTop,避免重置滚动位置 + this.scrollIntoView = ''; + this.$nextTick(() => { - // 先置空,再设置 ID,触发滚动 - this.scrollIntoView = ''; - + // 等待 DOM 更新 setTimeout(() => { + // 设置 scroll-into-view 为底部锚点,触发滚动 this.scrollIntoView = 'chat-bottom-anchor'; - }, 100); + }, 200); // 增加延时,等待图片等资源加载完成 }); }, + /** + * 加载历史消息后保持滚动位置 + * 修复:先清空 scroll-into-view 避免冲突,使用 uni.createSelectorQuery 准确计算高度差 + */ scrollToOldLastMessage() { + // 先清空 scroll-into-view,避免优先级冲突 + this.scrollIntoView = ''; + this.$nextTick(() => { - this.getScrollHeight(); - const newHeight = this.scrollViewHeight; - this.scrollTop = newHeight - this.oldScrollViewHeight; + // 使用 uni.createSelectorQuery 获取准确的滚动高度 + const query = uni.createSelectorQuery().in(this); + query.select('.message-list-container').boundingClientRect((rect) => { + if (rect) { + const newHeight = rect.height; + // 计算新旧高度差,设置 scroll-top + const heightDiff = newHeight - this.oldScrollViewHeight; + if (heightDiff > 0) { + this.scrollTop = heightDiff; + // 更新旧高度记录 + this.oldScrollViewHeight = newHeight; + } + } + }).exec(); }); }, async loadHistory() { if (!this.hasMore || this.loading) return; + // 在加载历史消息前,记录当前滚动高度 + this.getScrollHeight(); + this.loading = true; try { if (typeof ChatManager !== 'undefined') {