// pages/contact/index.js const request = require('../../api/request.js'); Page({ /** * 页面的初始数据 */ data: { messages: [], inputText: '', scrollToId: '', // 使用本地头像 userAvatar: '/static/user.png', serviceAvatar: '/static/contact.png', showGetUser: false, userInfo: {}, appid: '', page: 1, page_size: 100, openid: '', total: 0, // 是否滚动在底部 isAtBottom: true, // 正在加载更多(顶部分页) loadingMore: false, }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { const accountInfo = wx.getAccountInfoSync(); this.setData({ appid: accountInfo.miniProgram.appId, }); // 检查是否已有用户信息 if (wx.getStorageSync('user_info')) { this.setData({ showGetUser: false, userInfo: wx.getStorageSync('user_info'), }); this.getMessages() // start polling when page loads and user is present this.startPolling(); } else { // 直接进行静默登录,不显示授权弹窗 this.silentLogin(); } }, // onShow() { // this.getMessages() // }, getMessages() { const userInfo = wx.getStorageSync('user_info'); const that = this; request('app/messages', 'get', { app_id: that.data.appid, user_id: userInfo.openid, page: that.data.page, page_size: that.data.page_size, }).then(res => { // 处理消息列表,使用统一的消息处理函数 const list = that.processMessages(res.list); // 根据ID进行排序,确保ID最大的(最新的)消息在最后 const sortedList = that.sortMessagesByID(list); that.setData({ messages: sortedList, 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 }); }) }, // Load older messages (previous pages) and prepend them to the top loadMoreAtTop() { if (this.data.loadingMore) return; // if we've loaded all messages, skip const already = this.data.messages.length || 0; if (already >= this.data.total) return; // compute next page to fetch const nextPage = this.data.page + 1; this.setData({ loadingMore: true }); const userInfo = wx.getStorageSync('user_info'); const that = this; // capture current top-most visible message id to preserve scroll position const prevTopId = this.data.messages[0] ? this.data.messages[0].id : null; request('app/messages', 'get', { app_id: this.data.appid, user_id: userInfo.openid, page: nextPage, page_size: this.data.page_size, }).then(res => { // 处理新加载的消息,使用统一的消息处理函数 const newList = that.processMessages(res.list); // 合并新消息和现有消息 const combined = newList.concat(that.data.messages); // 根据ID进行排序,确保ID最大的(最新的)消息在最后 const sortedCombined = that.sortMessagesByID(combined); that.setData({ messages: sortedCombined, page: nextPage, total: res.total, }, () => { // after render, keep the previous top message in view if (prevTopId) { // scroll-into-view to the previous top id so viewport doesn't jump that.setData({ scrollToId: prevTopId }); } that.setData({ loadingMore: false }); }); }).catch(() => { that.setData({ loadingMore: false }); }); }, // 统一的消息处理函数 processMessages(messageList) { return messageList.map((item, index) => { // 优先使用服务器返回的ID,如果没有则使用创建时间戳 if (item.id && !item.id.toString().startsWith('m') && !item.id.toString().startsWith('u')) { item.id = 'm' + item.id; } else if (item.create_time) { // 使用创建时间的时间戳作为ID item.id = 'm' + new Date(item.create_time).getTime(); } else if (!item.id) { // 兜底方案:使用当前时间戳 + 索引 item.id = 'm' + (Date.now() + index); } // 解析消息内容 if (typeof item.content === 'string') { try { item.content = JSON.parse(item.content); } catch (e) { console.warn('消息内容解析失败:', item.content); item.content = { messages: item.content }; } } return item; }); }, // 消息排序函数 sortMessagesByID(messages) { return messages.sort((a, b) => { // 提取ID的数字部分 const getNumericId = (id) => { if (id.startsWith('u')) { // 用户消息:使用时间戳 return parseInt(id.replace('u', '')) || 0; } else if (id.startsWith('m')) { // 服务器消息:如果是时间戳格式(长度>10),直接使用;否则转换为时间戳格式 const numId = parseInt(id.replace('m', '')) || 0; // 如果是小数字(比如1,2,3),可能是服务器的序号ID,需要转换为可比较的时间戳 // 这里假设小于1000000000000(约2001年)的都是序号ID if (numId < 1000000000000) { // 将小的序号ID映射到一个较早的时间范围,确保它们排在用户消息之前 return numId + 1000000000; // 加上一个基数,但仍然小于用户消息的时间戳 } return numId; } return 0; }; const idA = getNumericId(a.id); const idB = getNumericId(b.id); return idA - idB; }); }, // 静默登录方法,只需要appid和js_code silentLogin() { const that = this; const accountInfo = wx.getAccountInfoSync(); wx.login({ success: function (res) { request('wechat/miniprogram/login', 'post', { "app_id": accountInfo.miniProgram.appId, "js_code": res.code }).then(resp => { wx.setStorageSync('user_info', resp); that.setData({ showGetUser: false, userInfo: resp }); request('app/user/create', 'post', { "app_id": accountInfo.miniProgram.appId, "user_avatar": resp.user_avatar || '/static/user.png', "user_id": resp.openid, "user_name": resp.user_name || '微信用户' }).then(resp => { that.getMessages() // start polling once user info exists that.startPolling(); }); }).catch(err => { console.error('静默登录失败:', err); // 如果静默登录失败,可以选择显示授权弹窗或其他处理 that.setData({ showGetUser: true }); }); }, fail: function(err) { console.error('wx.login失败:', err); } }); }, // Start polling messages every 1 second. Ensures only one interval exists. startPolling() { if (this.poller) return; // already polling // immediate fetch then periodic this.getMessages(); this.poller = setInterval(() => { this.getMessages(); }, 1000); }, // Stop polling and clear interval stopPolling() { if (this.poller) { clearInterval(this.poller); this.poller = null; } }, // Scroll handlers: stop polling when user scrolls up, resume when reach bottom onScroll(e) { // e.detail.scrollTop increases when scrolling down. We want to detect upward scroll. const scrollTop = e.detail.scrollTop || 0; const last = this._lastScrollTop || 0; // If user scrolls up (new scrollTop < last), pause polling if (scrollTop < last) { // user scrolled up if (this.data.isAtBottom) { // only act if we were previously at bottom this.setData({ isAtBottom: false }); this.stopPolling(); } } // detect reaching near top to load more if (scrollTop <= 2) { // if there are more messages to load and not already loading if (!this.data.loadingMore && this.data.messages.length < this.data.total) { this.loadMoreAtTop(); } } // update lastScrollTop for next event this._lastScrollTop = scrollTop; }, onScrollToLower() { // user scrolled to bottom (or very near). resume polling if needed if (!this.data.isAtBottom) { this.setData({ isAtBottom: true }); this.startPolling(); // 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 : ''; if (lastId) { this.setData({ scrollToId: lastId }); } } }, onReady() { // 初始化欢迎消息,带时间分割线 const now = new Date(); const timeStr = this.formatTime(now); this.setData({ messages: [], scrollToId: 'm1' }); }, onInput(e) { this.setData({ inputText: e.detail.value }); }, sendText() { const text = this.data.inputText && this.data.inputText.trim(); if (!text) return; const now = new Date(); const id = 'u' + Date.now(); // const timeStr = this.formatTime(now); // // 判断是否需要显示时间分割线 // let showTime = false; // const lastMsg = this.data.messages.length ? this.data.messages[this.data.messages.length - 1] : null; // if (!lastMsg || now - (lastMsg._ts || now) > 5 * 60 * 1000) showTime = true; const msg = { id, content: { messages: text }, "msg_type": 1, receiver_id: '', send_time: "刚刚", sender_id: this.data.userInfo.openid, sender_name: this.data.userInfo.user_name }; // 添加新消息并重新排序 const newMessages = this.data.messages.concat(msg); const sortedMessages = this.sortMessagesByID(newMessages); this.setData({ messages: sortedMessages, inputText: '' }); const accountInfo = wx.getAccountInfoSync(); const userInfo = wx.getStorageSync('user_info') request('app/send_message', 'POST', { "app_id": accountInfo.miniProgram.appId, "content": JSON.stringify({ messages: text }), "from_user_id": userInfo.openid, "from_user_name": userInfo.user_name, "msg_type": 1 }) // 等待渲染,确保 scroll-into-view 生效 setTimeout(() => { // 滚动到最新的消息(排序后的最后一条) const latestMessage = sortedMessages[sortedMessages.length - 1]; this.setData({ scrollToId: latestMessage ? latestMessage.id : id }); }, 50); // 模拟客服回复(延迟) // setTimeout(() => { // const replyNow = new Date(); // const replyTimeStr = this.formatTime(replyNow); // const reply = { // id: 's' + Date.now(), // from: 'service', // type: 'text', // content: '收到,客服正在处理中...', // showTime: false, // timeStr: replyTimeStr, // _ts: replyNow // }; // this.setData({ // messages: this.data.messages.concat(reply), // scrollToId: reply.id // }); // }, 800); }, chooseImage() { const that = this; wx.chooseImage({ count: 1, // 选择图片的数量 mediaType: ['image'], // 仅选择图片 sizeType: ['compressed', 'original'], sourceType: ['album', 'camera'], success(res) { const tempFilePaths = res.tempFilePaths; console.log('选择的图片路径:', res); if (tempFilePaths && tempFilePaths.length) { const userInfo = wx.getStorageSync('user_info') // const now = new Date(); const id = 'u' + Date.now(); // const timeStr = that.formatTime(now); // let showTime = false; // const lastMsg = that.data.messages.length ? that.data.messages[that.data.messages.length - 1] : null; // if (!lastMsg || now - (lastMsg._ts || now) > 5 * 60 * 1000) showTime = true; const msg = { id, msg_type: '2', receiver_id: '', content: { messages: tempFilePaths[0] }, sender_id: userInfo.openid, sender_name: userInfo.user_name, receiver_id: '', send_time: "刚刚", }; const list = that.data.messages list.push(msg); that.setData({ messages: list }); setTimeout(() => { that.setData({ scrollToId: id }); }, 50); wx.uploadFile({ filePath: tempFilePaths[0], // 图片临时文件路径 name: 'file', // 服务器接收文件的字段名,需与后端对应 url: 'https://mini-chat.1024tool.vip/admin/upload/image', // 服务器接收图片的接口地址 success: (res) => { const data = JSON.parse(res.data); console.log('上传成功', data); const accountInfo = wx.getAccountInfoSync(); request('app/send_message', 'POST', { "app_id": accountInfo.miniProgram.appId, "content": JSON.stringify({ messages: 'https://mini-chat.1024tool.vip/'+ data.preview_image_url }), "from_user_id": userInfo.openid, "from_user_name": userInfo.user_name, "msg_type": 2 }) }, fail: (err) => { console.error('上传失败', err); } }); // 模拟客服返回图片确认 // setTimeout(() => { // const replyNow = new Date(); // const replyTimeStr = that.formatTime(replyNow); // const reply = { // id: 's' + Date.now(), // from: 'service', // type: 'text', // content: '已收到图片,感谢!', // showTime: false, // timeStr: replyTimeStr, // _ts: replyNow // }; // that.setData({ // messages: that.data.messages.concat(reply) // }); // setTimeout(() => { // that.setData({ // scrollToId: reply.id // }); // }, 50); // }, 1200); } } }); }, // 时间格式化 formatTime(date) { const y = date.getFullYear(); const m = (date.getMonth() + 1).toString().padStart(2, '0'); const d = date.getDate().toString().padStart(2, '0'); const h = date.getHours().toString().padStart(2, '0'); const min = date.getMinutes().toString().padStart(2, '0'); return `${y}年${m}月${d}日 ${h}:${min}`; }, previewImage(e) { const src = e.currentTarget.dataset.src; if (!src) return; wx.previewImage({ urls: [src], current: src }); }, onShow() { // resume polling when page becomes visible if (wx.getStorageSync('user_info')) { this.startPolling(); } }, onHide() { // stop polling when leaving page this.stopPolling(); }, onUnload() { // cleanup this.stopPolling(); }, onPullDownRefresh() { }, onReachBottom() { }, onShareAppMessage() { } });