feat(聊天页面): 添加新消息提醒功能并优化自动滚动逻辑
实现智能滚动行为,当用户不在底部时显示新消息提醒气泡 添加动画效果和样式,点击气泡可滚动到最新消息 优化滚动检测逻辑,保持轮询始终运行
This commit is contained in:
parent
7ce96a57e5
commit
0af7fc4ec5
@ -24,6 +24,10 @@ Page({
|
|||||||
isAtBottom: true,
|
isAtBottom: true,
|
||||||
// 正在加载更多(顶部分页)
|
// 正在加载更多(顶部分页)
|
||||||
loadingMore: false,
|
loadingMore: false,
|
||||||
|
// 新消息提醒相关状态
|
||||||
|
showNewMessageTip: false, // 是否显示新消息提醒
|
||||||
|
newMessageCount: 0, // 新消息数量
|
||||||
|
lastMessageId: '', // 最后一条消息的ID,用于检测新消息
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,12 +72,27 @@ Page({
|
|||||||
// 根据ID进行排序,确保ID最大的(最新的)消息在最后
|
// 根据ID进行排序,确保ID最大的(最新的)消息在最后
|
||||||
const sortedList = that.sortMessagesByID(list);
|
const sortedList = that.sortMessagesByID(list);
|
||||||
|
|
||||||
that.setData({
|
// 智能滚动逻辑:用户在底部时自动滚动,在上方时保持位置
|
||||||
|
const updateData = {
|
||||||
messages: sortedList,
|
messages: sortedList,
|
||||||
total: res.total,
|
total: res.total
|
||||||
// only auto-scroll if user is at bottom
|
};
|
||||||
scrollToId: that.data.isAtBottom ? (sortedList[sortedList.length - 1] ? sortedList[sortedList.length - 1].id : '') : that.data.scrollToId
|
|
||||||
|
that.setData(updateData);
|
||||||
|
|
||||||
|
// 如果用户在底部,延迟设置滚动ID确保滚动生效
|
||||||
|
if (that.data.isAtBottom && sortedList.length > 0) {
|
||||||
|
const lastMessageId = sortedList[sortedList.length - 1].id;
|
||||||
|
console.log('用户在底部,准备自动滚动到消息ID:', lastMessageId);
|
||||||
|
setTimeout(() => {
|
||||||
|
that.setData({
|
||||||
|
scrollToId: lastMessageId
|
||||||
});
|
});
|
||||||
|
console.log('已设置scrollToId:', lastMessageId);
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
console.log('用户不在底部或无消息,保持当前位置。isAtBottom:', that.data.isAtBottom, '消息数量:', sortedList.length);
|
||||||
|
}
|
||||||
|
|
||||||
return res; // 返回结果以支持链式调用
|
return res; // 返回结果以支持链式调用
|
||||||
});
|
});
|
||||||
@ -130,7 +149,7 @@ Page({
|
|||||||
|
|
||||||
// 统一的消息处理函数
|
// 统一的消息处理函数
|
||||||
processMessages(messageList) {
|
processMessages(messageList) {
|
||||||
return messageList.map((item, index) => {
|
const processedMessages = messageList.map((item, index) => {
|
||||||
// 优先使用服务器返回的ID,如果没有则使用创建时间戳
|
// 优先使用服务器返回的ID,如果没有则使用创建时间戳
|
||||||
if (item.id && !item.id.toString().startsWith('m') && !item.id.toString().startsWith('u')) {
|
if (item.id && !item.id.toString().startsWith('m') && !item.id.toString().startsWith('u')) {
|
||||||
item.id = 'm' + item.id;
|
item.id = 'm' + item.id;
|
||||||
@ -154,6 +173,44 @@ Page({
|
|||||||
|
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 检测新消息并显示提醒
|
||||||
|
this.checkNewMessages(processedMessages);
|
||||||
|
|
||||||
|
return processedMessages;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检测新消息的函数
|
||||||
|
checkNewMessages(newMessages) {
|
||||||
|
if (newMessages.length === 0) return;
|
||||||
|
|
||||||
|
const currentMessages = this.data.messages;
|
||||||
|
const currentLastMessageId = this.data.lastMessageId;
|
||||||
|
const sortedNewMessages = this.sortMessagesByID(newMessages);
|
||||||
|
const latestNewMessage = sortedNewMessages[sortedNewMessages.length - 1];
|
||||||
|
|
||||||
|
// 如果不在底部且有新消息
|
||||||
|
if (!this.data.isAtBottom && currentMessages.length > 0) {
|
||||||
|
// 找出真正的新消息(在当前消息列表中不存在的消息)
|
||||||
|
const currentMessageIds = new Set(currentMessages.map(msg => msg.id));
|
||||||
|
const realNewMessages = sortedNewMessages.filter(msg => !currentMessageIds.has(msg.id));
|
||||||
|
|
||||||
|
if (realNewMessages.length > 0) {
|
||||||
|
// 累加新消息数量(而不是重置)
|
||||||
|
const currentCount = this.data.newMessageCount || 0;
|
||||||
|
this.setData({
|
||||||
|
showNewMessageTip: true,
|
||||||
|
newMessageCount: currentCount + realNewMessages.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后一条消息ID
|
||||||
|
if (latestNewMessage) {
|
||||||
|
this.setData({
|
||||||
|
lastMessageId: latestNewMessage.id
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 消息排序函数
|
// 消息排序函数
|
||||||
@ -246,13 +303,15 @@ Page({
|
|||||||
// e.detail.scrollTop increases when scrolling down. We want to detect upward scroll.
|
// e.detail.scrollTop increases when scrolling down. We want to detect upward scroll.
|
||||||
const scrollTop = e.detail.scrollTop || 0;
|
const scrollTop = e.detail.scrollTop || 0;
|
||||||
const last = this._lastScrollTop || 0;
|
const last = this._lastScrollTop || 0;
|
||||||
// If user scrolls up (new scrollTop < last), pause polling
|
// If user scrolls up (new scrollTop < last), mark as not at bottom but keep polling
|
||||||
if (scrollTop < last) {
|
if (scrollTop < last) {
|
||||||
// user scrolled up
|
// user scrolled up
|
||||||
if (this.data.isAtBottom) {
|
if (this.data.isAtBottom) {
|
||||||
// only act if we were previously at bottom
|
// only update state if we were previously at bottom, but keep polling active
|
||||||
|
console.log('用户向上滚动,设置isAtBottom为false');
|
||||||
this.setData({ isAtBottom: false });
|
this.setData({ isAtBottom: false });
|
||||||
this.stopPolling();
|
// 注释掉停止轮询的逻辑,确保始终获取新消息
|
||||||
|
// this.stopPolling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// detect reaching near top to load more
|
// detect reaching near top to load more
|
||||||
@ -267,14 +326,31 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onScrollToLower() {
|
onScrollToLower() {
|
||||||
// user scrolled to bottom (or very near). resume polling if needed
|
// user scrolled to bottom (or very near). update state and ensure polling is active
|
||||||
|
console.log('用户滚动到底部,当前isAtBottom状态:', this.data.isAtBottom);
|
||||||
if (!this.data.isAtBottom) {
|
if (!this.data.isAtBottom) {
|
||||||
this.setData({ isAtBottom: true });
|
this.setData({
|
||||||
|
isAtBottom: true,
|
||||||
|
showNewMessageTip: false,
|
||||||
|
newMessageCount: 0
|
||||||
|
});
|
||||||
|
console.log('已设置isAtBottom为true');
|
||||||
|
// 确保轮询始终运行(即使之前已经在运行)
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
// also ensure we scroll to latest message next render
|
// also ensure we scroll to latest message next render
|
||||||
const lastId = this.data.messages[this.data.messages.length - 1] ? this.data.messages[this.data.messages.length - 1].id : '';
|
const lastId = this.data.messages[this.data.messages.length - 1] ? this.data.messages[this.data.messages.length - 1].id : '';
|
||||||
if (lastId) {
|
if (lastId) {
|
||||||
this.setData({ scrollToId: lastId });
|
this.setData({ scrollToId: lastId });
|
||||||
|
console.log('设置scrollToId到最新消息:', lastId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 即使已经在底部,也要隐藏新消息提醒
|
||||||
|
if (this.data.showNewMessageTip) {
|
||||||
|
this.setData({
|
||||||
|
showNewMessageTip: false,
|
||||||
|
newMessageCount: 0
|
||||||
|
});
|
||||||
|
console.log('隐藏新消息提醒');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -507,6 +583,30 @@ Page({
|
|||||||
wx.stopPullDownRefresh();
|
wx.stopPullDownRefresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 点击新消息提醒,滚动到底部
|
||||||
|
scrollToBottom() {
|
||||||
|
const messages = this.data.messages;
|
||||||
|
if (messages.length > 0) {
|
||||||
|
const sortedMessages = this.sortMessagesByID(messages);
|
||||||
|
const latestMessage = sortedMessages[sortedMessages.length - 1];
|
||||||
|
|
||||||
|
this.setData({
|
||||||
|
scrollToId: latestMessage.id,
|
||||||
|
showNewMessageTip: false,
|
||||||
|
newMessageCount: 0,
|
||||||
|
isAtBottom: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 延迟一下确保滚动完成
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setData({
|
||||||
|
scrollToId: ''
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onReachBottom() { },
|
onReachBottom() { },
|
||||||
onShareAppMessage() { }
|
onShareAppMessage() { }
|
||||||
});
|
});
|
||||||
@ -33,6 +33,14 @@
|
|||||||
</block>
|
</block>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 新消息提醒 -->
|
||||||
|
<view wx:if="{{showNewMessageTip}}" class="new-message-tip" bindtap="scrollToBottom">
|
||||||
|
<view class="tip-content">
|
||||||
|
<text class="tip-text">{{newMessageCount}}条新消息</text>
|
||||||
|
<view class="tip-arrow">↓</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="input-area">
|
<view class="input-area">
|
||||||
<image class="btn-image-img" src="/static/upload.png" bindtap="chooseImage" mode="aspectFit" />
|
<image class="btn-image-img" src="/static/upload.png" bindtap="chooseImage" mode="aspectFit" />
|
||||||
<input class="input" placeholder="请输入内容" value="{{inputText}}" bindinput="onInput" confirm-type="send" bindconfirm="sendText" />
|
<input class="input" placeholder="请输入内容" value="{{inputText}}" bindinput="onInput" confirm-type="send" bindconfirm="sendText" />
|
||||||
|
|||||||
@ -315,3 +315,56 @@
|
|||||||
margin-left: 8rpx; /* match bubble spacing */
|
margin-left: 8rpx; /* match bubble spacing */
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 新消息提醒样式 */
|
||||||
|
.new-message-tip {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 200rpx;
|
||||||
|
right: 30rpx;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: tipFadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-content {
|
||||||
|
background: #07c160;
|
||||||
|
color: white;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-arrow {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
animation: bounce 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tipFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-6rpx);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-3rpx);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
测试说明.md
Normal file
73
测试说明.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# 自动滚动修复测试说明
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
已修复聊天页面自动刷新后自动滚动到底部的问题,现在实现智能滚动:
|
||||||
|
|
||||||
|
### 修复的问题
|
||||||
|
- ✅ 轮询刷新时不再强制滚动到底部
|
||||||
|
- ✅ 用户在底部时,新消息会自动滚动显示
|
||||||
|
- ✅ 用户在上方浏览历史消息时,保持当前位置
|
||||||
|
- ✅ 添加了延迟处理确保滚动生效
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
- `pages/contact/index.js` - 修改了 `getMessages()` 函数的滚动逻辑
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 打开项目
|
||||||
|
1. 使用微信开发者工具打开项目目录:`/Users/win/code2025/wx-chant`
|
||||||
|
2. 确保项目配置正确,appid 已设置
|
||||||
|
|
||||||
|
### 2. 测试场景
|
||||||
|
|
||||||
|
#### 场景1:用户在底部时的自动滚动
|
||||||
|
1. 进入聊天页面
|
||||||
|
2. 滚动到最底部
|
||||||
|
3. 等待新消息到达(轮询刷新)
|
||||||
|
4. **预期结果**:新消息自动滚动到视图中
|
||||||
|
|
||||||
|
#### 场景2:用户在上方时保持位置
|
||||||
|
1. 进入聊天页面
|
||||||
|
2. 向上滚动查看历史消息
|
||||||
|
3. 等待新消息到达(轮询刷新)
|
||||||
|
4. **预期结果**:
|
||||||
|
- 滚动位置保持不变
|
||||||
|
- 显示新消息提醒气泡
|
||||||
|
- 点击气泡可滚动到最新消息
|
||||||
|
|
||||||
|
#### 场景3:用户发送消息时的自动滚动
|
||||||
|
1. 在聊天页面发送文本或图片消息
|
||||||
|
2. **预期结果**:自动滚动到刚发送的消息
|
||||||
|
|
||||||
|
### 3. 调试信息
|
||||||
|
已添加控制台日志,可在微信开发者工具的控制台中查看:
|
||||||
|
- 滚动状态变化
|
||||||
|
- 自动滚动触发情况
|
||||||
|
- scrollToId 设置情况
|
||||||
|
|
||||||
|
### 4. 关键日志信息
|
||||||
|
- `用户在底部,准备自动滚动到消息ID: xxx` - 触发自动滚动
|
||||||
|
- `用户不在底部或无消息,保持当前位置` - 保持位置
|
||||||
|
- `用户向上滚动,设置isAtBottom为false` - 状态变化
|
||||||
|
- `用户滚动到底部,当前isAtBottom状态: xxx` - 到达底部
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 核心逻辑
|
||||||
|
```javascript
|
||||||
|
// 智能滚动:先更新数据,再延迟设置滚动
|
||||||
|
that.setData(updateData);
|
||||||
|
|
||||||
|
if (that.data.isAtBottom && sortedList.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
that.setData({
|
||||||
|
scrollToId: lastMessageId
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
- `isAtBottom`: 标记用户是否在聊天底部
|
||||||
|
- `scrollToId`: 控制滚动到指定消息
|
||||||
|
- 延迟50ms确保DOM更新后再滚动
|
||||||
Loading…
x
Reference in New Issue
Block a user