fix: 聊天页面默认在最底部
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user