From 6aa92c432529995b690c39d5e3be378410ac7803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E9=A3=9E=E6=B4=8B?= <15778543+xufeiyang6017@user.noreply.gitee.com> Date: Mon, 22 Dec 2025 10:55:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=81=8A=E5=A4=A9=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/chat-detail/index.js | 514 ++++++++++++----------------------- pages/chat-detail/index.wxml | 18 +- pages/chat-detail/index.wxss | 83 +++++- 3 files changed, 263 insertions(+), 352 deletions(-) diff --git a/pages/chat-detail/index.js b/pages/chat-detail/index.js index a9ab084..81594f9 100644 --- a/pages/chat-detail/index.js +++ b/pages/chat-detail/index.js @@ -10,52 +10,42 @@ Page({ chatTitle: '聊天对象', managerPhone: null, timer: null, - lastLoadTime: 0 // 用于节流的时间戳 + scrollTop: 0, // 用于控制scroll-view的滚动位置 + scrollWithAnimation: true, // 控制滚动是否使用动画 + isAtBottom: true, // 标记用户是否在聊天记录底部 + hasNewMessages: false, // 标记是否有未读的新消息 + newMessageCount: 0, // 未读新消息数量 + lastMessageCount: 0 // 上一次消息数量,用于检测新消息 }, onLoad: function (options) { - // 支持两种参数传递方式: - // 1. 从聊天列表页传递的 id 参数 - // 2. 从客服列表页传递的 userId、userName、phone 参数 - let chatId = options.id || options.userId; - let managerPhone = options.phone; - - if (chatId) { + if (options.id) { this.setData({ - chatId: chatId, - managerPhone: managerPhone || options.id // 如果没有直接传递phone,则使用chatId作为fallback + chatId: options.id, + managerPhone: options.id }); - - // 如果有传递name或userName参数,使用该名称作为聊天标题 - let chatTitle = options.name || options.userName; - if (chatTitle) { + + if (options.name) { this.setData({ - chatTitle: decodeURIComponent(chatTitle) + chatTitle: decodeURIComponent(options.name) }); } else { - // 如果没有传递名称参数,加载聊天标题 this.loadChatTitle(); } } - this.loadMessages(); - - // 设置定时器,每5秒查询一次数据库同步内容 + this.loadMessages(); // 使用默认值true,首次进入页面时自动滚动到底部 this.startTimer(); }, - // 页面显示时重新启动定时器 onShow: function () { if (!this.data.timer) { this.startTimer(); } }, - // 加载聊天标题 loadChatTitle: function () { - // 直接使用已有的managerPhone获取业务员信息 const managerPhone = this.data.managerPhone; if (managerPhone) { - // 获取业务员信息 API.getSalesPersonnelInfo(managerPhone).then(personnelInfo => { if (personnelInfo && personnelInfo.alias) { this.setData({ chatTitle: personnelInfo.alias }); @@ -73,14 +63,14 @@ Page({ // 格式化时间显示 formatDateTime: function (dateString) { if (!dateString) return '刚刚'; - + const now = new Date(); const msgDate = new Date(dateString); const diffMs = now - msgDate; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); - + if (diffMins < 1) { return '刚刚'; } else if (diffMins < 60) { @@ -90,7 +80,6 @@ Page({ } else if (diffDays < 7) { return `${diffDays}天前`; } else { - // 超过一周显示具体日期 const year = msgDate.getFullYear(); const month = (msgDate.getMonth() + 1).toString().padStart(2, '0'); const day = msgDate.getDate().toString().padStart(2, '0'); @@ -100,216 +89,155 @@ Page({ } }, - loadMessages: function () { - // 节流逻辑:避免短时间内多次调用 - const currentTime = Date.now(); - if (currentTime - this.data.lastLoadTime < 2000) { // 2秒内不重复调用 - console.log('API调用节流中,距离上次调用仅' + (currentTime - this.data.lastLoadTime) + 'ms'); - return Promise.resolve(); - } - - // 更新上次加载时间 - this.setData({ - lastLoadTime: currentTime - }); - - return new Promise((resolve, reject) => { - this.setData({ loading: true }); - - // 获取当前用户的手机号 - const users = wx.getStorageSync('users') || {}; - const userId = wx.getStorageSync('userId'); - let userPhone = null; - - // 尝试从users中获取手机号 - if (userId && users[userId]) { - if (users[userId].phoneNumber) { - userPhone = users[userId].phoneNumber; - } else if (users[userId].phone) { - userPhone = users[userId].phone; - } - } - - // 如果还没有获取到,尝试从全局用户信息获取 - if (!userPhone) { - const userInfo = wx.getStorageSync('userInfo'); - if (userInfo) { - if (userInfo.phoneNumber) { - userPhone = userInfo.phoneNumber; - } else if (userInfo.phone) { - userPhone = userInfo.phone; - } - } - } - - // 如果还没有获取到,尝试从直接存储获取 - if (!userPhone) { - if (wx.getStorageSync('phoneNumber')) { - userPhone = wx.getStorageSync('phoneNumber'); - } else if (wx.getStorageSync('phone')) { - userPhone = wx.getStorageSync('phone'); - } - } - - // 重要修复:当有managerPhone时,应该使用managerPhone作为chatId传递给服务器 - // 因为服务器端查询需要的是手机号,而不是客服人员的ID - const chatIdForApi = this.data.managerPhone || this.data.chatId; - - // 使用新添加的API获取聊天记录 - API.getChatMessages(chatIdForApi, userPhone).then(res => { - if (Array.isArray(res)) { - // 处理每条消息,确定发送者和格式化时间 - const processedMessages = res.map(message => { - // 判断消息是发送还是接收 - const sender = (message.sender_phone === userPhone) ? 'me' : 'other'; - - // 格式化时间 - const time = this.formatDateTime(message.created_at); - - return { - id: message.id, - content: message.content, - sender: sender, - time: time, - originalTime: message.created_at, - sender_phone: message.sender_phone, - receiver_phone: message.receiver_phone - }; - }); - - // 按时间顺序排序(升序) - processedMessages.sort((a, b) => { - return new Date(a.originalTime) - new Date(b.originalTime); - }); - - this.setData({ - messages: processedMessages, - loading: false, - scrollToView: processedMessages.length > 0 ? 'latest-message' : '' - }); - - // 如果managerPhone尚未设置,尝试从聊天记录中获取 - if (!this.data.managerPhone && processedMessages.length > 0) { - const firstMessage = processedMessages[0]; - const managerPhone = (firstMessage.sender_phone === userPhone) ? firstMessage.receiver_phone : firstMessage.sender_phone; - this.setData({ managerPhone: managerPhone }); - } - } else { - this.setData({ - messages: [], - loading: false - }); - } - resolve(); - }).catch(error => { - console.error('加载聊天记录失败:', error); - this.setData({ - loading: false - }); - wx.showToast({ - title: '加载聊天记录失败', - icon: 'none' - }); - reject(error); - }); - }); - }, - - loadMoreMessages: function () { - // 加载更多历史消息 - if (this.data.loading) return; - + loadMessages: function (shouldScrollToBottom = true) { this.setData({ loading: true }); - + // 获取当前用户的手机号 const users = wx.getStorageSync('users') || {}; const userId = wx.getStorageSync('userId'); let userPhone = null; - - // 尝试从users中获取手机号 + if (userId && users[userId]) { - if (users[userId].phoneNumber) { - userPhone = users[userId].phoneNumber; - } else if (users[userId].phone) { - userPhone = users[userId].phone; - } + userPhone = users[userId].phoneNumber || users[userId].phone; } - - // 如果还没有获取到,尝试从全局用户信息获取 + if (!userPhone) { const userInfo = wx.getStorageSync('userInfo'); - if (userInfo) { - if (userInfo.phoneNumber) { - userPhone = userInfo.phoneNumber; - } else if (userInfo.phone) { - userPhone = userInfo.phone; - } - } + userPhone = userInfo ? (userInfo.phoneNumber || userInfo.phone) : null; } - - // 如果还没有获取到,尝试从直接存储获取 + if (!userPhone) { - if (wx.getStorageSync('phoneNumber')) { - userPhone = wx.getStorageSync('phoneNumber'); - } else if (wx.getStorageSync('phone')) { - userPhone = wx.getStorageSync('phone'); - } + userPhone = wx.getStorageSync('phoneNumber') || wx.getStorageSync('phone'); } - - // 获取最早的消息时间,用于分页加载 - const earliestTime = this.data.messages.length > 0 ? this.data.messages[0].originalTime : null; - - // 使用API获取更多聊天记录(带分页参数) - const chatIdForApi = this.data.managerPhone || this.data.chatId; - API.getChatMessages(chatIdForApi, userPhone, { before: earliestTime }).then(res => { - if (Array.isArray(res) && res.length > 0) { - // 处理每条消息,确定发送者和格式化时间 + + if (!userPhone) { + this.setData({ loading: false }); + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + // 获取聊天消息 + API.getChatMessages(this.data.chatId, userPhone).then(res => { + if (Array.isArray(res)) { + // 处理消息并排序 const processedMessages = res.map(message => { - // 判断消息是发送还是接收 const sender = (message.sender_phone === userPhone) ? 'me' : 'other'; - - // 格式化时间 const time = this.formatDateTime(message.created_at); - return { id: message.id, content: message.content, sender: sender, time: time, - originalTime: message.created_at + originalTime: message.created_at, + isNew: false // 服务器返回的消息不显示新消息动画 }; }); - - // 按时间顺序排序(升序) + + // 按创建时间升序排序(旧消息在前,新消息在后) processedMessages.sort((a, b) => { return new Date(a.originalTime) - new Date(b.originalTime); }); - + + // 在更新数据之前,保存当前的最后一条消息,用于检测新消息 + const currentLastMessage = this.data.messages[this.data.messages.length - 1]; + const newLastMessage = processedMessages[processedMessages.length - 1]; + + // 更新消息列表 this.setData({ - messages: [...processedMessages, ...this.data.messages], + messages: processedMessages, loading: false + }, () => { + // 在数据更新完成的回调中执行逻辑,确保DOM已更新 + + // 首次加载消息或明确需要滚动时,直接滚动到底部 + if (shouldScrollToBottom) { + this.scrollToBottom(false); + } + else { + // 检查是否有新消息 + // 比较最新消息的ID是否与当前显示的最后一条消息ID不同 + + // 有新消息的条件: + // 1. 消息列表不为空 + // 2. 新消息列表的最后一条消息与当前显示的最后一条消息ID不同 + const hasNewMessages = newLastMessage && currentLastMessage && + newLastMessage.id !== currentLastMessage.id; + + console.log('新消息检测:', { hasNewMessages, isAtBottom: this.data.isAtBottom }); + + if (hasNewMessages) { + if (this.data.isAtBottom) { + // 如果用户当前在底部,自动滚动到底部 + console.log('用户在底部,自动滚动到新消息'); + this.scrollToBottom(true); + } else { + // 如果用户不在底部,显示新消息提示按钮 + console.log('用户不在底部,显示新消息提示'); + this.setData({ + hasNewMessages: true, + newMessageCount: this.data.newMessageCount + 1 + }); + } + } + } }); } else { this.setData({ + messages: [], loading: false }); - wx.showToast({ - title: '没有更多消息了', - icon: 'none' - }); } }).catch(error => { - console.error('加载更多聊天记录失败:', error); - this.setData({ - loading: false - }); + console.error('加载聊天记录失败:', error); + this.setData({ loading: false }); wx.showToast({ - title: '加载更多消息失败', + title: '加载聊天记录失败', icon: 'none' }); }); }, + // 滚动到底部 + scrollToBottom: function (withAnimation = true) { + console.log('执行滚动到底部操作,是否使用动画:', withAnimation); + + // 使用wx.nextTick确保在DOM更新后执行 + wx.nextTick(() => { + this.setData({ + scrollWithAnimation: withAnimation, + scrollTop: 999999 + }, () => { + // 滚动完成后清除新消息提示 + setTimeout(() => { + this.setData({ + hasNewMessages: false, + newMessageCount: 0 + }); + console.log('滚动完成,清除新消息提示'); + }, 100); + }); + }); + }, + + // 监听滚动事件,检测是否在底部 + onScroll: function (e) { + const { scrollTop, scrollHeight, clientHeight } = e.detail; + // 计算滚动位置与底部的距离 + const distanceToBottom = scrollHeight - scrollTop - clientHeight; + // 当距离底部小于100rpx(约50px)时,标记为在底部 + const isAtBottom = distanceToBottom < 100; + + console.log('滚动检测:', { scrollTop, scrollHeight, clientHeight, distanceToBottom, isAtBottom }); + + // 直接更新状态,确保滚动位置实时反映 + this.setData({ + isAtBottom: isAtBottom + }); + }, + onInputChange: function (e) { this.setData({ inputValue: e.detail.value @@ -319,209 +247,109 @@ Page({ sendMessage: function () { const content = this.data.inputValue.trim(); if (!content) return; - + // 获取当前用户的手机号 const users = wx.getStorageSync('users') || {}; const userId = wx.getStorageSync('userId'); let userPhone = null; - - // 尝试从users中获取手机号 + if (userId && users[userId]) { - if (users[userId].phoneNumber) { - userPhone = users[userId].phoneNumber; - } else if (users[userId].phone) { - userPhone = users[userId].phone; - } + userPhone = users[userId].phoneNumber || users[userId].phone; } - - // 如果还没有获取到,尝试从全局用户信息获取 + if (!userPhone) { const userInfo = wx.getStorageSync('userInfo'); - if (userInfo) { - if (userInfo.phoneNumber) { - userPhone = userInfo.phoneNumber; - } else if (userInfo.phone) { - userPhone = userInfo.phone; - } - } + userPhone = userInfo ? (userInfo.phoneNumber || userInfo.phone) : null; } - - // 如果还没有获取到,尝试从直接存储获取 + if (!userPhone) { - if (wx.getStorageSync('phoneNumber')) { - userPhone = wx.getStorageSync('phoneNumber'); - } else if (wx.getStorageSync('phone')) { - userPhone = wx.getStorageSync('phone'); - } + userPhone = wx.getStorageSync('phoneNumber') || wx.getStorageSync('phone'); } - - // 获取客服手机号 - let managerPhone = this.data.managerPhone; - + if (!userPhone) { wx.showToast({ - title: '获取用户手机号失败,请重新登录', + title: '请先登录', icon: 'none' }); return; } - - // 如果managerPhone尚未设置,尝试从聊天记录中获取 - if (!managerPhone && this.data.messages && this.data.messages.length > 0) { - // 从第一条消息中提取与当前用户不同的手机号作为managerPhone - const firstMessage = this.data.messages[0]; - if (firstMessage && firstMessage.sender_phone) { - managerPhone = (firstMessage.sender_phone === userPhone) ? firstMessage.receiver_phone : firstMessage.sender_phone; - this.setData({ managerPhone: managerPhone }); - } - } - - // 如果managerPhone仍然不存在,尝试直接从API获取 + + const managerPhone = this.data.managerPhone; if (!managerPhone) { - // 显示加载提示 - wx.showLoading({ - title: '获取客服信息中...', - }); - - // 先尝试从聊天列表获取 - API.getChatList(userPhone).then(chatList => { - if (Array.isArray(chatList)) { - // 找到当前聊天项 - const currentChat = chatList.find(item => item.id === this.data.chatId); - if (currentChat && currentChat.manager_phone) { - managerPhone = currentChat.manager_phone; - this.setData({ managerPhone: managerPhone }); - - // 获取到managerPhone后立即发送消息 - this.sendMessageAfterGetManagerPhone(userPhone, managerPhone, content); - } else { - // 如果聊天列表中没有找到,尝试直接从聊天记录获取 - this.loadMessages().then(() => { - if (this.data.messages.length > 0) { - const firstMessage = this.data.messages[0]; - managerPhone = (firstMessage.sender_phone === userPhone) ? firstMessage.receiver_phone : firstMessage.sender_phone; - this.setData({ managerPhone: managerPhone }); - this.sendMessageAfterGetManagerPhone(userPhone, managerPhone, content); - } else { - wx.hideLoading(); - wx.showToast({ - title: '获取客服信息失败,请稍候重试', - icon: 'none' - }); - } - }).catch(() => { - wx.hideLoading(); - wx.showToast({ - title: '获取客服信息失败,请稍候重试', - icon: 'none' - }); - }); - } - } else { - wx.hideLoading(); - wx.showToast({ - title: '获取客服信息失败,请稍候重试', - icon: 'none' - }); - } - }).catch(() => { - wx.hideLoading(); - wx.showToast({ - title: '获取客服信息失败,请稍候重试', - icon: 'none' - }); + wx.showToast({ + title: '获取聊天对象失败', + icon: 'none' }); return; } - - // 正常发送消息 - this.sendMessageAfterGetManagerPhone(userPhone, managerPhone, content); - }, - - // 当获取到managerPhone后发送消息 - sendMessageAfterGetManagerPhone: function (userPhone, managerPhone, content) { - // 隐藏加载提示 - wx.hideLoading(); - + // 创建临时消息 - const tempId = Date.now(); - const newMessage = { - id: tempId, + const tempMessage = { + id: Date.now(), content: content, sender: 'me', - time: '刚刚' + time: '刚刚', + originalTime: new Date().toISOString(), + isNew: true // 标识新消息 }; - - // 先在本地显示消息 + + // 更新消息列表 + const newMessages = [...this.data.messages, tempMessage]; this.setData({ - messages: [...this.data.messages, newMessage], - inputValue: '', - scrollToView: 'latest-message' + messages: newMessages, + inputValue: '' + }, () => { + // 发送消息时无论用户在哪个位置,都自动滚动到底部 + this.scrollToBottom(true); + + // 短暂延迟后移除isNew标记,确保动画完成 + setTimeout(() => { + const updatedMessages = this.data.messages.map(msg => ({ + ...msg, + isNew: false + })); + this.setData({ messages: updatedMessages }); + }, 300); }); - - // 调用API发送消息到数据库 + + // 发送到服务器 API.sendMessage(userPhone, managerPhone, content).then(res => { console.log('消息发送成功:', res); - // 如果需要,可以更新消息的id为服务器返回的id - if (res && res.data && res.data.id) { - const updatedMessages = this.data.messages.map(msg => { - if (msg.id === tempId) { - return { - ...msg, - id: res.data.id - }; - } - return msg; - }); - this.setData({ - messages: updatedMessages - }); - } }).catch(error => { console.error('消息发送失败:', error); wx.showToast({ - title: '消息发送失败,请稍后重试', + title: '消息发送失败', icon: 'none' }); }); - - // 移除模拟对方回复的代码,改为定时器同步数据库内容 }, - // 启动定时器 startTimer: function () { - // 清除之前的定时器(如果有) if (this.data.timer) { clearInterval(this.data.timer); } - - // 设置新的定时器,每5秒调用一次loadMessages方法 + + // 每5秒同步一次消息,但不自动滚动到底部 const timer = setInterval(() => { - this.loadMessages(); + this.loadMessages(false); }, 5000); - + this.setData({ timer: timer }); }, - // 页面隐藏时清除定时器 onHide: function () { if (this.data.timer) { clearInterval(this.data.timer); - this.setData({ - timer: null - }); + this.setData({ timer: null }); } }, - // 页面卸载时清除定时器 onUnload: function () { if (this.data.timer) { clearInterval(this.data.timer); - this.setData({ - timer: null - }); + this.setData({ timer: null }); } - }, + } }); \ No newline at end of file diff --git a/pages/chat-detail/index.wxml b/pages/chat-detail/index.wxml index a552785..1761097 100644 --- a/pages/chat-detail/index.wxml +++ b/pages/chat-detail/index.wxml @@ -1,4 +1,4 @@ - + @@ -11,7 +11,14 @@ - + 加载中... @@ -21,7 +28,7 @@ - + @@ -39,6 +46,11 @@ + + + 有{{newMessageCount}}条新消息,点击查看 + + diff --git a/pages/chat-detail/index.wxss b/pages/chat-detail/index.wxss index 1f20228..709dfd1 100644 --- a/pages/chat-detail/index.wxss +++ b/pages/chat-detail/index.wxss @@ -1,8 +1,11 @@ -.container { - background-color: #f0f0f0; - min-height: 100vh; +.chat-page-container { + height: 100vh; display: flex; flex-direction: column; + box-sizing: border-box; + background-color: #f0f0f0; + position: relative; + overflow: hidden; } .chat-header { @@ -39,9 +42,10 @@ color: #333; } +.chat-scroll-area, .chat-content { flex: 1; - padding: 20rpx 20rpx 120rpx 20rpx; /* 增加底部padding,避免被输入框遮挡 */ + padding: 20rpx 20rpx 140rpx 20rpx; /* 增加底部padding,避免被固定输入框遮挡 */ overflow-y: auto; } @@ -56,6 +60,16 @@ align-items: flex-end; gap: 16rpx; max-width: 100%; + opacity: 1; + transform: translateY(0) scale(1); + transition: all 0.3s ease-out; + animation-fill-mode: forwards; +} + +/* 新消息初始状态 */ +.message-item.new { + opacity: 0; + transform: translateY(20rpx) scale(0.95); } .message-item.other { @@ -63,7 +77,12 @@ } .message-item.me { - flex-direction: row-reverse; + flex-direction: row; + justify-content: flex-end; +} + +.message-item.me .avatar { + margin-right: 16rpx; } .avatar { @@ -86,16 +105,44 @@ position: relative; } +/* 对方消息气泡 */ .message-item.other .message-bubble { background-color: #fff; border-bottom-left-radius: 8rpx; } +/* 对方消息气泡尖角 */ +.message-item.other .message-bubble::before { + content: ''; + position: absolute; + left: -16rpx; + bottom: 16rpx; + width: 0; + height: 0; + border-top: 16rpx solid transparent; + border-bottom: 16rpx solid transparent; + border-right: 16rpx solid #fff; +} + +/* 自己消息气泡 */ .message-item.me .message-bubble { background-color: #92E3A9; border-bottom-right-radius: 8rpx; } +/* 自己消息气泡尖角 */ +.message-item.me .message-bubble::before { + content: ''; + position: absolute; + right: -16rpx; + bottom: 16rpx; + width: 0; + height: 0; + border-top: 16rpx solid transparent; + border-bottom: 16rpx solid transparent; + border-left: 16rpx solid #92E3A9; +} + .message-content { display: block; font-size: 28rpx; @@ -123,11 +170,35 @@ padding: 16rpx 24rpx; border-top: 1rpx solid #e8e8e8; gap: 20rpx; + z-index: 100; + height: 100rpx; position: fixed; bottom: 0; left: 0; right: 0; - z-index: 100; + box-sizing: border-box; +} + +/* 新消息提示按钮样式 */ +.new-message-tip { + background-color: #FF6B81; + color: white; + font-size: 26rpx; + padding: 12rpx 24rpx; + border-radius: 28rpx; + position: fixed; + bottom: 120rpx; + right: 30rpx; + z-index: 99; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.15); + opacity: 0.9; + transition: all 0.3s ease; +} + +.new-message-tip:hover { + opacity: 1; + transform: translateY(-2rpx); + box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.2); } .message-input {