2025-11-06 13:51:28 +08:00

710 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pages/contact/index.js
const request = require('../../api/request.js');
const uploadFile = require('../../api/upload.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,
// 新消息提醒
showNewMessageTip: false, // 是否显示新消息提醒
newMessageCount: 0, // 新消息数量
lastMessageId: '', // 最后一条消息的ID用于检测新消息
// 欢迎消息标记
hasWelcomeMessageSent: 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().then(() => {
// 在获取消息完成后发送欢迎消息
this.sendWelcomeMessage();
});
// 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;
return 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);
// 智能滚动逻辑:用户在底部时自动滚动,在上方时保持位置
const updateData = {
messages: sortedList,
total: res.total
};
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; // 返回结果以支持链式调用
});
},
// 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) {
const processedMessages = 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 {
const text = JSON.parse(item.content)
item.content = {
message: text.message || text.messages
};
} catch (e) {
console.warn('消息内容解析失败:', item.content);
item.content = { message: item.content };
}
}
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
});
}
},
// 消息排序函数
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().then(() => {
// 在获取消息完成后发送欢迎消息
that.sendWelcomeMessage();
});
// 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), mark as not at bottom but keep polling
if (scrollTop < last) {
// user scrolled up
if (this.data.isAtBottom) {
// only update state if we were previously at bottom, but keep polling active
console.log('用户向上滚动设置isAtBottom为false');
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). update state and ensure polling is active
console.log('用户滚动到底部当前isAtBottom状态:', this.data.isAtBottom);
if (!this.data.isAtBottom) {
this.setData({
isAtBottom: true,
showNewMessageTip: false,
newMessageCount: 0
});
console.log('已设置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 });
console.log('设置scrollToId到最新消息:', lastId);
}
} else {
// 即使已经在底部,也要隐藏新消息提醒
if (this.data.showNewMessageTip) {
this.setData({
showNewMessageTip: false,
newMessageCount: 0
});
console.log('隐藏新消息提醒');
}
}
},
onReady() {
// 初始化欢迎消息,带时间分割线
const now = new Date();
const timeStr = this.formatTime(now);
this.setData({
messages: [],
scrollToId: 'm1'
});
},
onInput(e) {
this.setData({
inputText: e.detail.value
});
},
// 发送欢迎消息
sendWelcomeMessage() {
// 检查用户信息是否存在
if (!this.data.userInfo || !this.data.userInfo.openid) {
console.log('用户信息不存在,无法发送欢迎消息');
return;
}
// 使用用户ID作为标记检查是否已经发送过欢迎消息
const welcomeKey = `welcome_sent_${this.data.userInfo.openid}`;
const hasWelcomeSent = wx.getStorageSync(welcomeKey);
if (hasWelcomeSent) {
console.log('该用户的欢迎消息已发送过,跳过');
this.setData({ hasWelcomeMessageSent: true });
return;
}
const welcomeText = '你好';
const now = new Date();
const id = 'welcome_' + Date.now();
const msg = {
id,
content: {
message: welcomeText
},
"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,
hasWelcomeMessageSent: true // 标记已发送欢迎消息
});
const accountInfo = wx.getAccountInfoSync();
const userInfo = wx.getStorageSync('user_info');
console.log('发送欢迎消息:', welcomeText);
// 发送到服务器
request('app/send_message', 'POST', {
"app_id": accountInfo.miniProgram.appId,
"content": JSON.stringify({
message: welcomeText
}),
"from_user_id": userInfo.openid,
"from_user_name": userInfo.user_name,
"msg_type": 1
}).then(() => {
console.log('欢迎消息发送成功');
// 保存标记到本地存储,表示该用户已发送过欢迎消息
wx.setStorageSync(welcomeKey, true);
}).catch(err => {
console.error('欢迎消息发送失败:', err);
// 如果发送失败,不保存标记,下次还可以重试
});
// 等待渲染,确保 scroll-into-view 生效
setTimeout(() => {
// 滚动到最新的消息(排序后的最后一条)
const latestMessage = sortedMessages[sortedMessages.length - 1];
this.setData({
scrollToId: latestMessage ? latestMessage.id : id
});
}, 50);
},
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: {
message: 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({
message: 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: {
message: 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);
uploadFile(tempFilePaths[0]).then((res) => {
const accountInfo = wx.getAccountInfoSync();
request('app/send_message', 'POST', {
"app_id": accountInfo.miniProgram.appId,
"content": JSON.stringify({
message: res
}),
"from_user_id": userInfo.openid,
"from_user_name": userInfo.user_name,
"msg_type": 2
}).catch((err) => {
wx.showToast({
title: '图片发送失败',
icon: 'none'
});
});
}).catch((err) => {
wx.showToast({
title: '图片发送失败',
icon: 'none'
});
});
// wx.uploadFile({
// filePath: tempFilePaths[0], // 图片临时文件路径
// name: 'file', // 服务器接收文件的字段名,需与后端对应
// url: 'https://mini-chat.1024tool.vip/api/admin/upload/image', // 服务器接收图片的接口地址
// success: (res) => {
// },
// 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({
// message: 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 and get latest messages
if (wx.getStorageSync('user_info')) {
// 立即请求最新消息
this.getMessages();
// 然后开始轮询
this.startPolling();
}
},
onHide() {
// stop polling when leaving page
this.stopPolling();
},
onUnload() {
// cleanup
this.stopPolling();
},
onPullDownRefresh() {
// 下拉刷新时获取最新消息
if (wx.getStorageSync('user_info')) {
this.getMessages().then(() => {
// 停止下拉刷新动画
wx.stopPullDownRefresh();
}).catch(() => {
wx.stopPullDownRefresh();
});
} else {
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() { },
onShareAppMessage() { }
});