fix: 聊天页面默认在最底部

This commit is contained in:
2025-12-23 10:30:10 +08:00
parent 72848ff2f6
commit 4580404dfd

View File

@@ -45,11 +45,13 @@
<!-- 修复: 增加 :scroll-into-view 绑定这是更稳定的自动滚动方案 -->
<scroll-view
class="chat-messages"
:style="scrollViewStyle"
scroll-y="true"
:scroll-into-view="scrollIntoView"
:scroll-top="scrollTop"
scroll-with-animation
@scrolltoupper="loadHistory"
@scroll="onScroll"
>
<view class="message-list-container" id="msglist">
<!-- 无更多消息提示 -->
@@ -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') {