与牧同行-小程序用户端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1415 lines
38 KiB

  1. import http from '../../../utils/api'
  2. const baseUrl = require('../../../utils/baseUrl')
  3. // WebSocket实例
  4. let socketOpen = false
  5. let socketMsgQueue = []
  6. let heartbeatInterval = null
  7. let reconnectTimer = null
  8. let isManualClose = false // 是否手动关闭
  9. let heartBeatTimer = null // 心跳定时器
  10. Page({
  11. data: {
  12. baseUrl: baseUrl,
  13. // 专家信息
  14. expertInfo: {},
  15. // 用户信息
  16. userInfo: {
  17. user: {} // 完整用户信息
  18. },
  19. // 对话信息
  20. conversation: {},
  21. // 消息列表
  22. messageList: [],
  23. scrollTop: 0,
  24. scrollAnimate: false,
  25. // 输入相关
  26. inputValue: '',
  27. inputFocus: false,
  28. // 多媒体
  29. showMediaSheet: false,
  30. // 页面状态
  31. showDateDivider: true,
  32. todayDate: '',
  33. loading: false,
  34. loadingMore: false,
  35. // 滚动相关
  36. isScrolling: false,
  37. lastScrollTop: 0,
  38. // 当前对话ID和对方用户ID
  39. currentExpertId: '',
  40. conversationId: '',
  41. otherUserId: '',
  42. // 分页相关
  43. page: 1,
  44. pageSize: 20,
  45. hasMore: true,
  46. total: 0,
  47. // 记录最早消息的时间戳,用于加载更早的消息
  48. earliestMsgTime: 0,
  49. // 时间显示间隔
  50. timeInterval: 5,
  51. lastShowTimeStamp: 0,
  52. // 键盘高度
  53. keyboardHeight: 0,
  54. // WebSocket状态
  55. socketConnected: false,
  56. reconnectCount: 0,
  57. maxReconnectCount: 5,
  58. // 第一次加载完成标记
  59. firstLoadComplete: false,
  60. // 是否有新消息
  61. hasNewMessage: false,
  62. // 消息ID集合,用于去重
  63. messageIds: new Set(),
  64. // 正在发送中的消息ID集合
  65. sendingMessages: new Set(),
  66. // 显示发送中提示
  67. showSendingTip: false,
  68. // 发送中的消息数量
  69. sendingCount: 0
  70. },
  71. onLoad: function(options) {
  72. console.log('接收到的参数:', options);
  73. // 设置今天日期
  74. this.setTodayDate();
  75. // 加载用户信息
  76. this.loadUserInfo();
  77. // 获取对方用户ID
  78. if (options.id) {
  79. this.setData({
  80. otherUserId: options.id,
  81. currentExpertId: options.id,
  82. // 初始页码设置为1
  83. page: 1,
  84. messageList: [],
  85. hasMore: true,
  86. earliestMsgTime: 0,
  87. firstLoadComplete: false,
  88. messageIds: new Set(),
  89. sendingMessages: new Set(),
  90. showSendingTip: false,
  91. sendingCount: 0
  92. });
  93. // 先创建对话,然后获取聊天记录
  94. this.createConversation(options.id);
  95. }
  96. // 监听键盘高度变化
  97. wx.onKeyboardHeightChange(this.onKeyboardHeightChange.bind(this));
  98. },
  99. onShow: function() {
  100. // 页面显示时连接WebSocket
  101. isManualClose = false;
  102. // 确保用户ID和对话ID都存在再连接
  103. const userId = this.getCurrentUserId();
  104. if (userId && this.data.conversationId) {
  105. console.log('页面显示,准备连接WebSocket,用户ID:', userId);
  106. this.connectWebSocket();
  107. } else {
  108. console.log('用户ID或对话ID不存在,稍后再连接', {
  109. userId: userId,
  110. conversationId: this.data.conversationId
  111. });
  112. }
  113. },
  114. onHide: function() {
  115. // 页面隐藏时暂停心跳检查但不关闭连接
  116. console.log('页面隐藏,暂停心跳检查');
  117. if (heartBeatTimer) {
  118. clearInterval(heartBeatTimer);
  119. heartBeatTimer = null;
  120. }
  121. },
  122. onUnload: function() {
  123. // 页面卸载时关闭WebSocket并清理资源
  124. isManualClose = true;
  125. this.closeWebSocket();
  126. wx.offKeyboardHeightChange();
  127. // 清理定时器
  128. if (heartbeatInterval) {
  129. clearInterval(heartbeatInterval);
  130. heartbeatInterval = null;
  131. }
  132. if (reconnectTimer) {
  133. clearTimeout(reconnectTimer);
  134. reconnectTimer = null;
  135. }
  136. if (heartBeatTimer) {
  137. clearInterval(heartBeatTimer);
  138. heartBeatTimer = null;
  139. }
  140. },
  141. // 获取当前用户ID的辅助方法
  142. getCurrentUserId: function() {
  143. const userId = this.data.userInfo?.user?.userId;
  144. console.log('获取到的用户ID:', userId);
  145. return userId;
  146. },
  147. // 创建一对一聊天
  148. createConversation(id) {
  149. wx.showLoading({ title: '加载中...' });
  150. http.create({
  151. data: {
  152. otherUserId: id
  153. },
  154. success: res => {
  155. console.log('创建对话响应:', res);
  156. if (res && res.data) {
  157. // 保存对话ID
  158. const conversationData = res.data;
  159. this.setData({
  160. conversationId: conversationData.id || conversationData.conversationId || id,
  161. conversation: conversationData,
  162. otherUserId: id,
  163. // 重置分页参数
  164. page: 1,
  165. hasMore: true,
  166. messageList: [],
  167. earliestMsgTime: 0,
  168. firstLoadComplete: false,
  169. messageIds: new Set(),
  170. sendingMessages: new Set()
  171. });
  172. // 获取聊天记录(从第一页开始)
  173. this.getChatHistory(id, false);
  174. // 连接WebSocket
  175. const userId = this.getCurrentUserId();
  176. if (userId) {
  177. console.log('用户ID已存在,立即连接WebSocket');
  178. this.connectWebSocket();
  179. } else {
  180. console.log('用户ID尚未加载,等待用户信息加载完成');
  181. }
  182. } else {
  183. wx.hideLoading();
  184. wx.showToast({
  185. title: '创建对话失败',
  186. icon: 'none'
  187. });
  188. }
  189. },
  190. fail: err => {
  191. console.error('创建对话失败:', err);
  192. wx.hideLoading();
  193. wx.showToast({
  194. title: '网络错误',
  195. icon: 'none'
  196. });
  197. }
  198. });
  199. },
  200. // 获取聊天记录
  201. getChatHistory(id, isLoadMore = false) {
  202. // 如果是加载更多且没有更多数据,直接返回
  203. if (isLoadMore && !this.data.hasMore) {
  204. console.log('没有更多消息了');
  205. this.setData({ loadingMore: false });
  206. return;
  207. }
  208. // 如果不是加载更多,显示加载提示
  209. if (!isLoadMore) {
  210. wx.showLoading({ title: '加载中...' });
  211. }
  212. const params = {
  213. otherUserId: id,
  214. page: this.data.page,
  215. pageSize: this.data.pageSize
  216. };
  217. console.log('请求聊天记录参数:', params);
  218. http.direct({
  219. data: params,
  220. success: res => {
  221. console.log('获取聊天记录响应:', res);
  222. if (!isLoadMore) {
  223. wx.hideLoading();
  224. }
  225. if (res && res.code === 200) {
  226. // 处理返回的消息数据
  227. this.processChatHistory(res, isLoadMore);
  228. } else {
  229. // 如果没有聊天记录,显示空状态
  230. this.setData({
  231. messageList: [],
  232. loading: false,
  233. loadingMore: false,
  234. hasMore: false,
  235. firstLoadComplete: true
  236. });
  237. if (!isLoadMore) {
  238. wx.showToast({
  239. title: res?.msg || '获取聊天记录失败',
  240. icon: 'none'
  241. });
  242. }
  243. }
  244. },
  245. fail: err => {
  246. console.error('获取聊天记录失败:', err);
  247. if (!isLoadMore) {
  248. wx.hideLoading();
  249. }
  250. wx.showToast({
  251. title: '网络错误',
  252. icon: 'none'
  253. });
  254. this.setData({
  255. messageList: [],
  256. loading: false,
  257. loadingMore: false,
  258. firstLoadComplete: true
  259. });
  260. }
  261. });
  262. },
  263. // 处理聊天历史数据
  264. processChatHistory(response, isLoadMore = false) {
  265. let messages = [];
  266. let total = 0;
  267. // 根据实际返回的数据结构调整这里
  268. if (response.rows && Array.isArray(response.rows)) {
  269. messages = response.rows;
  270. total = response.total || messages.length;
  271. } else if (response.data && Array.isArray(response.data)) {
  272. messages = response.data;
  273. total = response.total || messages.length;
  274. } else if (Array.isArray(response)) {
  275. messages = response;
  276. total = messages.length;
  277. }
  278. console.log('处理消息数据:', {
  279. messageCount: messages.length,
  280. total,
  281. isLoadMore,
  282. currentPage: this.data.page
  283. });
  284. // 格式化消息数据
  285. const formattedMessages = this.formatMessages(messages);
  286. // 判断是否还有更多数据
  287. const hasMore = this.data.page * this.data.pageSize < total;
  288. // 记录最早消息的时间戳(用于加载更多)
  289. let earliestTime = this.data.earliestMsgTime;
  290. if (formattedMessages.length > 0) {
  291. // 因为接口返回的是正序,第一条就是最早的
  292. earliestTime = formattedMessages[0].timestamp;
  293. }
  294. let newMessageList;
  295. let targetScrollTop = 0;
  296. // 构建消息ID集合
  297. const messageIds = new Set(this.data.messageIds);
  298. formattedMessages.forEach(msg => {
  299. if (msg.id) {
  300. messageIds.add(msg.id);
  301. }
  302. });
  303. if (isLoadMore) {
  304. // 加载更多:新加载的消息(更早的)应该放在现有消息列表的前面
  305. newMessageList = [...formattedMessages, ...this.data.messageList];
  306. // 记录新消息的总高度,用于滚动定位
  307. targetScrollTop = formattedMessages.length * 120; // 估算每条消息高度
  308. } else {
  309. // 首次加载
  310. newMessageList = formattedMessages;
  311. }
  312. // 重新处理消息时间显示
  313. const processedMessages = this.processMessageTimes(newMessageList);
  314. this.setData({
  315. messageList: processedMessages,
  316. messageIds: messageIds,
  317. loading: false,
  318. loadingMore: false,
  319. hasMore: hasMore,
  320. total: total,
  321. earliestMsgTime: earliestTime,
  322. firstLoadComplete: true
  323. }, () => {
  324. if (isLoadMore) {
  325. // 加载更多后,调整滚动位置以保持在当前查看的位置
  326. setTimeout(() => {
  327. this.setData({
  328. scrollAnimate: false,
  329. scrollTop: targetScrollTop
  330. }, () => {
  331. setTimeout(() => {
  332. this.setData({ scrollAnimate: true });
  333. }, 100);
  334. });
  335. }, 50);
  336. } else {
  337. // 首次加载,滚动到底部(显示最新消息)
  338. setTimeout(() => {
  339. this.scrollToBottom(false);
  340. }, 100);
  341. }
  342. });
  343. },
  344. // 格式化消息数据
  345. formatMessages(messages) {
  346. if (!messages || messages.length === 0) return [];
  347. const userId = this.getCurrentUserId();
  348. console.log('格式化消息使用的用户ID:', userId);
  349. return messages.map(msg => {
  350. // 统一使用contentType字段
  351. let contentType = msg.contentType || msg.type || 'text';
  352. let content = msg.content || '';
  353. // 判断发送者
  354. const isMe = msg.senderId === userId ||
  355. msg.fromUserId === userId ||
  356. msg.userId === userId ||
  357. msg.sendId === userId;
  358. return {
  359. id: msg.id || 'msg_' + Date.now() + Math.random(),
  360. isMe: isMe,
  361. sender: isMe ? 'user' : 'expert',
  362. senderId: msg.senderId || msg.fromUserId || msg.userId,
  363. contentType: contentType,
  364. content: content,
  365. timestamp: msg.createTime || msg.timestamp || Date.now(),
  366. status: msg.status || 'success',
  367. fileName: msg.fileName || '',
  368. fileSize: msg.fileSize || 0,
  369. thumb: msg.thumb || '',
  370. progress: msg.progress || 100
  371. };
  372. });
  373. },
  374. // 加载更多消息
  375. loadMoreMessages: function() {
  376. // 防止重复加载
  377. if (this.data.loadingMore) {
  378. console.log('正在加载中,请稍后');
  379. return;
  380. }
  381. // 检查是否有更多数据
  382. if (!this.data.hasMore) {
  383. console.log('没有更多消息了');
  384. return;
  385. }
  386. // 检查对方用户ID是否存在
  387. if (!this.data.otherUserId) {
  388. console.error('对方用户ID不存在');
  389. return;
  390. }
  391. console.log('开始加载更多消息,当前页码:', this.data.page);
  392. // 设置加载状态
  393. this.setData({
  394. loadingMore: true
  395. }, () => {
  396. // 页码+1,加载更早的消息
  397. const nextPage = this.data.page + 1;
  398. this.setData({
  399. page: nextPage
  400. }, () => {
  401. // 调用获取聊天记录方法,传入true表示是加载更多操作
  402. this.getChatHistory(this.data.otherUserId, true);
  403. });
  404. });
  405. },
  406. // 加载用户信息
  407. loadUserInfo: function() {
  408. try {
  409. const userInfo = wx.getStorageSync('userInfo');
  410. console.log('从缓存加载的用户信息:', userInfo);
  411. if (userInfo) {
  412. // 处理用户信息,确保正确的结构
  413. let processedUserInfo = { ...this.data.userInfo };
  414. if (userInfo.user) {
  415. // 如果已经有user结构
  416. processedUserInfo = userInfo;
  417. } else if (userInfo.userId) {
  418. // 如果是直接返回的用户数据
  419. processedUserInfo = {
  420. user: userInfo
  421. };
  422. }
  423. // 确保有user对象
  424. if (!processedUserInfo.user) {
  425. processedUserInfo.user = {};
  426. }
  427. // 确保userId存在
  428. if (userInfo.userId && !processedUserInfo.user.userId) {
  429. processedUserInfo.user.userId = userInfo.userId;
  430. }
  431. // 设置用户ID
  432. const userId = processedUserInfo.user?.userId || userInfo.userId;
  433. if (userId) {
  434. processedUserInfo.id = userId;
  435. }
  436. console.log('处理后的用户信息:', processedUserInfo);
  437. this.setData({ userInfo: processedUserInfo });
  438. // 用户信息加载完成后,如果对话ID已存在,连接WebSocket
  439. if (this.data.conversationId && !this.data.socketConnected) {
  440. console.log('用户信息加载完成,连接WebSocket');
  441. this.connectWebSocket();
  442. }
  443. }
  444. } catch (e) {
  445. console.error('加载用户信息失败:', e);
  446. }
  447. },
  448. // ========== WebSocket相关方法 ==========
  449. // 连接WebSocket
  450. connectWebSocket() {
  451. // 如果已有连接,先检查是否可用
  452. if (socketOpen) {
  453. console.log('WebSocket已连接,无需重复连接');
  454. return;
  455. }
  456. const userId = this.getCurrentUserId();
  457. console.log('准备连接WebSocket,用户ID:', userId);
  458. if (!userId) {
  459. console.error('用户ID不存在,无法连接WebSocket');
  460. wx.showToast({
  461. title: '用户信息加载中,请稍后',
  462. icon: 'none'
  463. });
  464. return;
  465. }
  466. // WebSocket连接URL
  467. const basurl = '192.168.101.109:8080'; // 从配置文件获取
  468. const wsUrl = `ws://${basurl}/ws/mini/chat?userId=${userId}`;
  469. console.log('开始连接WebSocket:', wsUrl);
  470. wx.connectSocket({
  471. url: wsUrl,
  472. success: () => {
  473. console.log('WebSocket连接初始化成功');
  474. },
  475. fail: (err) => {
  476. console.error('WebSocket连接初始化失败:', err);
  477. this.reconnectWebSocket();
  478. }
  479. });
  480. // 监听连接打开
  481. wx.onSocketOpen(() => {
  482. console.log('WebSocket连接已打开');
  483. socketOpen = true;
  484. this.setData({ socketConnected: true, reconnectCount: 0 });
  485. // 发送队列中的消息
  486. while (socketMsgQueue.length > 0) {
  487. const msg = socketMsgQueue.shift();
  488. this.sendWebSocketMessage(msg);
  489. }
  490. // 开始心跳
  491. this.startHeartbeat();
  492. });
  493. // 监听收到消息
  494. wx.onSocketMessage((res) => {
  495. try {
  496. console.log('收到原始WebSocket消息:', res.data);
  497. const data = JSON.parse(res.data);
  498. console.log('解析后的消息:', data);
  499. // 处理不同类型的消息
  500. this.handleWebSocketMessage(data);
  501. } catch (e) {
  502. console.error('解析消息失败:', e, '原始数据:', res.data);
  503. }
  504. });
  505. // 监听连接关闭
  506. wx.onSocketClose(() => {
  507. console.log('WebSocket连接已关闭');
  508. socketOpen = false;
  509. this.setData({ socketConnected: false });
  510. // 停止心跳
  511. if (heartBeatTimer) {
  512. clearInterval(heartBeatTimer);
  513. heartBeatTimer = null;
  514. }
  515. // 如果不是手动关闭,尝试重连
  516. if (!isManualClose) {
  517. this.reconnectWebSocket();
  518. }
  519. });
  520. // 监听错误
  521. wx.onSocketError((err) => {
  522. console.error('WebSocket错误:', err);
  523. socketOpen = false;
  524. this.setData({ socketConnected: false });
  525. });
  526. },
  527. // 关闭WebSocket
  528. closeWebSocket() {
  529. if (socketOpen) {
  530. wx.closeSocket({
  531. success: () => {
  532. console.log('WebSocket连接已主动关闭');
  533. }
  534. });
  535. socketOpen = false;
  536. this.setData({ socketConnected: false });
  537. }
  538. if (heartBeatTimer) {
  539. clearInterval(heartBeatTimer);
  540. heartBeatTimer = null;
  541. }
  542. },
  543. // 重连WebSocket
  544. reconnectWebSocket() {
  545. if (this.data.reconnectCount >= this.data.maxReconnectCount) {
  546. console.log('达到最大重连次数,停止重连');
  547. return;
  548. }
  549. if (reconnectTimer) {
  550. clearTimeout(reconnectTimer);
  551. }
  552. reconnectTimer = setTimeout(() => {
  553. console.log(`尝试第${this.data.reconnectCount + 1}次重连`);
  554. this.setData({
  555. reconnectCount: this.data.reconnectCount + 1
  556. });
  557. this.connectWebSocket();
  558. }, 3000);
  559. },
  560. // 开始心跳
  561. startHeartbeat() {
  562. if (heartBeatTimer) {
  563. clearInterval(heartBeatTimer);
  564. }
  565. heartBeatTimer = setInterval(() => {
  566. if (socketOpen) {
  567. console.log('发送心跳');
  568. this.sendWebSocketMessage(JSON.stringify({
  569. type: 'heartbeat',
  570. userId: this.getCurrentUserId(),
  571. timestamp: Date.now()
  572. }));
  573. }
  574. }, 30000);
  575. },
  576. // 发送WebSocket消息
  577. sendWebSocketMessage(data) {
  578. if (socketOpen) {
  579. wx.sendSocketMessage({
  580. data: typeof data === 'string' ? data : JSON.stringify(data),
  581. success: () => {
  582. console.log('WebSocket消息发送成功:', data);
  583. },
  584. fail: (err) => {
  585. console.error('WebSocket消息发送失败:', err);
  586. socketMsgQueue.push(data);
  587. }
  588. });
  589. } else {
  590. console.log('WebSocket未连接,消息加入队列');
  591. socketMsgQueue.push(data);
  592. if (!isManualClose) {
  593. this.connectWebSocket();
  594. }
  595. }
  596. },
  597. // 处理WebSocket消息
  598. handleWebSocketMessage(data) {
  599. console.log('处理消息类型:', data.type);
  600. switch (data.type) {
  601. case 'chat':
  602. case 'message':
  603. this.handleNewMessage(data);
  604. break;
  605. case 'message_status':
  606. this.handleMessageStatus(data);
  607. break;
  608. case 'typing':
  609. this.handleTypingStatus(data);
  610. break;
  611. case 'online_status':
  612. this.handleOnlineStatus(data);
  613. break;
  614. case 'heartbeat_ack':
  615. console.log('收到心跳响应');
  616. break;
  617. default:
  618. console.log('未知消息类型:', data);
  619. }
  620. },
  621. // 处理新消息
  622. handleNewMessage(data) {
  623. console.log('处理新消息 - 完整数据:', data);
  624. // 提取消息内容
  625. let messageData = data.data || data;
  626. // 获取消息ID
  627. const messageId = messageData.id || messageData.messageId || 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  628. console.log('消息ID:', messageId);
  629. console.log('当前发送中消息集合:', this.data.sendingMessages);
  630. // 检查是否是自己发送的消息(通过sendingMessages集合判断)
  631. if (this.data.sendingMessages.has(messageId)) {
  632. console.log('收到自己发送的消息回执,跳过处理:', messageId);
  633. // 从发送中集合移除
  634. const sendingMessages = new Set(this.data.sendingMessages);
  635. sendingMessages.delete(messageId);
  636. this.setData({
  637. sendingMessages: sendingMessages,
  638. sendingCount: sendingMessages.size,
  639. showSendingTip: sendingMessages.size > 0
  640. });
  641. // 更新本地消息状态为成功
  642. this.updateMessageStatus(messageId, 'success');
  643. return;
  644. }
  645. // 检查消息是否已存在(去重)
  646. if (this.data.messageIds.has(messageId)) {
  647. console.log('消息已存在,跳过处理:', messageId);
  648. return;
  649. }
  650. const userId = this.getCurrentUserId();
  651. const senderId = messageData.senderId || messageData.fromUserId || messageData.userId;
  652. // 判断是否是自己发送的消息
  653. const isMe = senderId === userId;
  654. console.log('消息归属判断:', {
  655. senderId: senderId,
  656. userId: userId,
  657. isMe: isMe,
  658. messageId: messageId
  659. });
  660. // 如果是自己发送的消息但不在sendingMessages中,说明是通过其他方式发送的
  661. // 也跳过处理,避免重复
  662. if (isMe) {
  663. console.log('是自己发送的消息,但不在发送中集合,可能已存在,跳过处理');
  664. return;
  665. }
  666. // 创建新消息对象 - 统一使用contentType
  667. const newMessage = {
  668. id: messageId,
  669. isMe: isMe,
  670. sender: isMe ? 'user' : 'expert',
  671. senderId: senderId,
  672. contentType: messageData.contentType || messageData.type || 'text',
  673. content: messageData.content || '',
  674. timestamp: messageData.timestamp || Date.now(),
  675. status: 'success', // 对方消息直接显示成功
  676. progress: 100,
  677. fileName: messageData.fileName || '',
  678. fileSize: messageData.fileSize || 0,
  679. thumb: messageData.thumb || ''
  680. };
  681. console.log('创建的新消息对象:', newMessage);
  682. // 获取当前消息列表
  683. const currentList = [...this.data.messageList];
  684. console.log('当前消息列表长度:', currentList.length);
  685. // 添加新消息到列表末尾
  686. currentList.push(newMessage);
  687. // 更新消息ID集合
  688. const messageIds = new Set(this.data.messageIds);
  689. messageIds.add(messageId);
  690. // 重新处理时间显示
  691. const processedMessages = this.processMessageTimes(currentList);
  692. console.log('处理后的消息列表长度:', processedMessages.length);
  693. console.log('最后一条消息:', processedMessages[processedMessages.length - 1]);
  694. // 更新数据
  695. this.setData({
  696. messageList: processedMessages,
  697. messageIds: messageIds,
  698. hasNewMessage: !isMe // 如果不是自己发的,标记有新消息
  699. }, () => {
  700. console.log('消息列表已更新,当前列表:', this.data.messageList);
  701. // 判断是否需要滚动到底部
  702. const query = wx.createSelectorQuery();
  703. query.select('#chatScroll').boundingClientRect();
  704. query.select('.chat-bottom-space').boundingClientRect();
  705. query.exec((res) => {
  706. if (res[0] && res[1]) {
  707. const scrollHeight = res[0].height;
  708. const bottomOffset = res[1].top - res[0].top;
  709. const scrollTop = this.data.lastScrollTop;
  710. // 如果在底部附近(距离底部小于200px)或是自己发的消息,则滚动到底部
  711. const shouldScroll = isMe || (scrollTop + scrollHeight >= bottomOffset - 200);
  712. if (shouldScroll) {
  713. this.scrollToBottom(true);
  714. }
  715. } else {
  716. // 如果获取不到位置信息,默认滚动到底部
  717. if (isMe) {
  718. this.scrollToBottom(true);
  719. }
  720. }
  721. });
  722. // 如果不是自己发的消息,发送已读回执
  723. if (!isMe) {
  724. this.sendReadReceipt(messageId);
  725. }
  726. });
  727. },
  728. // 处理消息状态
  729. handleMessageStatus(data) {
  730. const { messageList } = this.data;
  731. const messageId = data.messageId || data.id;
  732. const index = messageList.findIndex(msg => msg.id === messageId);
  733. if (index !== -1) {
  734. messageList[index].status = data.status || 'success';
  735. this.setData({ messageList });
  736. }
  737. // 如果收到成功状态,从发送中集合移除
  738. if (data.status === 'success' || data.status === 'read') {
  739. const sendingMessages = new Set(this.data.sendingMessages);
  740. sendingMessages.delete(messageId);
  741. this.setData({
  742. sendingMessages: sendingMessages,
  743. sendingCount: sendingMessages.size,
  744. showSendingTip: sendingMessages.size > 0
  745. });
  746. }
  747. },
  748. // 处理正在输入状态
  749. handleTypingStatus(data) {
  750. const userId = data.userId || data.senderId;
  751. if (userId === this.data.otherUserId) {
  752. this.setData({ 'expertInfo.typing': true });
  753. clearTimeout(this.typingTimer);
  754. this.typingTimer = setTimeout(() => {
  755. this.setData({ 'expertInfo.typing': false });
  756. }, 3000);
  757. }
  758. },
  759. // 处理在线状态
  760. handleOnlineStatus(data) {
  761. const userId = data.userId || data.senderId;
  762. if (userId === this.data.otherUserId) {
  763. this.setData({
  764. 'expertInfo.online': data.online || data.status === 'online'
  765. });
  766. }
  767. },
  768. // 发送消息到服务器
  769. sendMessageToServer: function(content, contentType, messageId) {
  770. const receiverId = this.data.otherUserId;
  771. const senderId = this.getCurrentUserId();
  772. console.log('发送消息参数:', {
  773. senderId: senderId,
  774. receiverId: receiverId,
  775. content: content,
  776. contentType: contentType,
  777. messageId: messageId
  778. });
  779. if (!receiverId || !senderId) {
  780. console.error('发送者或接收者ID不存在', { senderId, receiverId });
  781. wx.showToast({
  782. title: '发送失败,用户信息错误',
  783. icon: 'none'
  784. });
  785. // 发送失败,从发送中集合移除
  786. const sendingMessages = new Set(this.data.sendingMessages);
  787. sendingMessages.delete(messageId);
  788. this.setData({
  789. sendingMessages: sendingMessages,
  790. sendingCount: sendingMessages.size,
  791. showSendingTip: sendingMessages.size > 0
  792. });
  793. // 更新消息状态为失败
  794. this.updateMessageStatus(messageId, 'failed');
  795. return;
  796. }
  797. const message = {
  798. type: 'chat',
  799. receiverId: receiverId,
  800. senderId: senderId,
  801. content: content,
  802. contentType: contentType,
  803. timestamp: Date.now(),
  804. messageId: messageId
  805. };
  806. const messageStr = JSON.stringify(message);
  807. console.log('发送消息到服务器:', JSON.parse(messageStr));
  808. if (!socketOpen) {
  809. console.log('WebSocket未连接,尝试连接');
  810. wx.showToast({
  811. title: '连接中,请稍后',
  812. icon: 'none'
  813. });
  814. this.connectWebSocket();
  815. socketMsgQueue.push(messageStr);
  816. return;
  817. }
  818. this.sendWebSocketMessage(messageStr);
  819. },
  820. // 发送已读回执
  821. sendReadReceipt(messageId) {
  822. if (!socketOpen) return;
  823. this.sendWebSocketMessage(JSON.stringify({
  824. type: 'message_status',
  825. messageId: messageId,
  826. status: 'read',
  827. userId: this.getCurrentUserId(),
  828. receiverId: this.data.otherUserId,
  829. conversationId: this.data.conversationId,
  830. timestamp: Date.now()
  831. }));
  832. },
  833. // 发送正在输入状态
  834. sendTypingStatus() {
  835. if (!socketOpen || !this.data.inputValue) return;
  836. this.sendWebSocketMessage(JSON.stringify({
  837. type: 'typing',
  838. userId: this.getCurrentUserId(),
  839. receiverId: this.data.otherUserId,
  840. conversationId: this.data.conversationId,
  841. timestamp: Date.now()
  842. }));
  843. },
  844. // ========== 消息发送相关方法 ==========
  845. // 发送文本消息
  846. sendTextMessage: function() {
  847. const content = this.data.inputValue.trim();
  848. if (!content) return;
  849. const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  850. console.log('发送文本消息,ID:', messageId);
  851. // 将消息ID添加到发送中集合
  852. const sendingMessages = new Set(this.data.sendingMessages);
  853. sendingMessages.add(messageId);
  854. // 创建本地消息 - 使用contentType
  855. const newMessage = {
  856. id: messageId,
  857. isMe: true,
  858. sender: 'user',
  859. senderId: this.getCurrentUserId(),
  860. contentType: 'text',
  861. content: content,
  862. timestamp: Date.now(),
  863. status: 'sending', // 设置为发送中状态
  864. progress: 100
  865. };
  866. // 添加到列表
  867. this.addMessageToList(newMessage);
  868. // 更新发送中集合和发送计数
  869. this.setData({
  870. sendingMessages: sendingMessages,
  871. sendingCount: sendingMessages.size,
  872. showSendingTip: sendingMessages.size > 0
  873. });
  874. // 清空输入框
  875. this.setData({ inputValue: '' });
  876. // 通过WebSocket发送到服务器
  877. this.sendMessageToServer(content, 'text', messageId);
  878. // 设置超时,如果5秒后还没有收到回执,认为发送失败
  879. setTimeout(() => {
  880. if (this.data.sendingMessages.has(messageId)) {
  881. console.log('消息发送超时:', messageId);
  882. this.updateMessageStatus(messageId, 'timeout');
  883. // 从发送中集合移除
  884. const sendingMessages = new Set(this.data.sendingMessages);
  885. sendingMessages.delete(messageId);
  886. this.setData({
  887. sendingMessages: sendingMessages,
  888. sendingCount: sendingMessages.size,
  889. showSendingTip: sendingMessages.size > 0
  890. });
  891. wx.showToast({
  892. title: '发送超时',
  893. icon: 'none'
  894. });
  895. }
  896. }, 5000);
  897. },
  898. // 上传图片后发送
  899. uploadImages: function(tempFilePaths) {
  900. tempFilePaths.forEach((tempFilePath, index) => {
  901. const fileName = `image_${Date.now()}_${index}.jpg`;
  902. this.uploadFile(tempFilePath, fileName, 0, 'image');
  903. });
  904. },
  905. // 上传视频后发送
  906. uploadVideo: function(tempFilePath, thumbPath) {
  907. const fileName = `video_${Date.now()}.mp4`;
  908. this.uploadFile(tempFilePath, fileName, 0, 'video', thumbPath);
  909. },
  910. // 通用文件上传
  911. uploadFile: function(tempFilePath, fileName, fileSize = 0, contentType = 'file', thumbPath = '') {
  912. const messageId = 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  913. // 将消息ID添加到发送中集合
  914. const sendingMessages = new Set(this.data.sendingMessages);
  915. sendingMessages.add(messageId);
  916. const message = {
  917. id: messageId,
  918. isMe: true,
  919. sender: 'user',
  920. contentType: contentType,
  921. content: tempFilePath,
  922. thumb: thumbPath,
  923. fileName: fileName,
  924. fileSize: fileSize,
  925. extension: fileName.split('.').pop().toLowerCase(),
  926. timestamp: Date.now(),
  927. status: 'uploading',
  928. progress: 0
  929. };
  930. this.addMessageToList(message);
  931. this.setData({
  932. sendingMessages: sendingMessages,
  933. sendingCount: sendingMessages.size,
  934. showSendingTip: sendingMessages.size > 0
  935. });
  936. this.simulateUpload(messageId, contentType, tempFilePath);
  937. },
  938. // 模拟上传
  939. simulateUpload: function(messageId, contentType, tempFilePath) {
  940. let progress = 0;
  941. const uploadInterval = setInterval(() => {
  942. progress += Math.random() * 20 + 10;
  943. if (progress >= 100) {
  944. progress = 100;
  945. clearInterval(uploadInterval);
  946. setTimeout(() => {
  947. this.updateMessageStatus(messageId, 'success');
  948. const { messageList } = this.data;
  949. const index = messageList.findIndex(msg => msg.id === messageId);
  950. if (index !== -1) {
  951. delete messageList[index].progress;
  952. this.setData({ messageList });
  953. const fileUrl = tempFilePath;
  954. this.sendMessageToServer(fileUrl, contentType, messageId);
  955. }
  956. }, 200);
  957. }
  958. const { messageList } = this.data;
  959. const index = messageList.findIndex(msg => msg.id === messageId);
  960. if (index !== -1) {
  961. messageList[index].progress = Math.min(Math.floor(progress), 100);
  962. this.setData({ messageList });
  963. }
  964. }, 100);
  965. },
  966. // 添加消息到列表
  967. addMessageToList: function(message) {
  968. const { messageList, messageIds } = this.data;
  969. // 检查是否已存在
  970. if (messageIds.has(message.id)) {
  971. console.log('消息已存在,不重复添加:', message.id);
  972. return;
  973. }
  974. // 新消息添加到末尾
  975. messageList.push(message);
  976. // 更新消息ID集合
  977. messageIds.add(message.id);
  978. // 重新处理时间显示
  979. const processedMessages = this.processMessageTimes(messageList);
  980. this.setData({
  981. messageList: processedMessages,
  982. messageIds: messageIds
  983. }, () => {
  984. this.scrollToBottom();
  985. });
  986. },
  987. // 更新消息状态
  988. updateMessageStatus: function(messageId, status) {
  989. const { messageList } = this.data;
  990. const index = messageList.findIndex(msg => msg.id === messageId);
  991. if (index !== -1) {
  992. messageList[index].status = status;
  993. this.setData({ messageList });
  994. // 如果是失败状态,从发送中集合移除
  995. if (status === 'failed' || status === 'timeout') {
  996. const sendingMessages = new Set(this.data.sendingMessages);
  997. sendingMessages.delete(messageId);
  998. this.setData({
  999. sendingMessages: sendingMessages,
  1000. sendingCount: sendingMessages.size,
  1001. showSendingTip: sendingMessages.size > 0
  1002. });
  1003. }
  1004. }
  1005. },
  1006. // 输入处理
  1007. onInput: function(e) {
  1008. this.setData({ inputValue: e.detail.value });
  1009. clearTimeout(this.typingDebounce);
  1010. this.typingDebounce = setTimeout(() => {
  1011. this.sendTypingStatus();
  1012. }, 500);
  1013. },
  1014. // 输入框获得焦点
  1015. onInputFocus: function() {
  1016. this.setData({ inputFocus: true }, () => {
  1017. setTimeout(() => {
  1018. this.scrollToBottom();
  1019. }, 200);
  1020. });
  1021. },
  1022. // 输入框失去焦点
  1023. onInputBlur: function() {
  1024. this.setData({ inputFocus: false });
  1025. },
  1026. // 清除输入
  1027. clearInput: function() {
  1028. this.setData({
  1029. inputValue: '',
  1030. inputFocus: true
  1031. });
  1032. },
  1033. // 滚动事件
  1034. onScroll: function(e) {
  1035. const scrollTop = e.detail.scrollTop;
  1036. this.setData({
  1037. lastScrollTop: scrollTop,
  1038. isScrolling: true
  1039. });
  1040. clearTimeout(this.scrollTimer);
  1041. this.scrollTimer = setTimeout(() => {
  1042. this.setData({ isScrolling: false });
  1043. }, 200);
  1044. // 当滚动到顶部附近时加载更多
  1045. if (scrollTop <= 50 && !this.data.loadingMore && this.data.hasMore && this.data.firstLoadComplete) {
  1046. console.log('触发加载更多,当前滚动位置:', scrollTop);
  1047. this.loadMoreMessages();
  1048. }
  1049. // 当滚动到底部时,清除新消息标记
  1050. const query = wx.createSelectorQuery();
  1051. query.select('#chatScroll').boundingClientRect();
  1052. query.select('.chat-bottom-space').boundingClientRect();
  1053. query.exec((res) => {
  1054. if (res[0] && res[1]) {
  1055. const scrollHeight = res[0].height;
  1056. const bottomOffset = res[1].top - res[0].top;
  1057. if (scrollTop + scrollHeight >= bottomOffset - 50) {
  1058. this.setData({ hasNewMessage: false });
  1059. }
  1060. }
  1061. });
  1062. },
  1063. // 滚动到底部
  1064. scrollToBottom: function(animate = true) {
  1065. if (this.data.isScrolling) return;
  1066. this.setData({
  1067. scrollAnimate: animate,
  1068. hasNewMessage: false
  1069. }, () => {
  1070. setTimeout(() => {
  1071. this.setData({ scrollTop: 999999 });
  1072. }, 50);
  1073. });
  1074. },
  1075. // 键盘高度变化
  1076. onKeyboardHeightChange: function(res) {
  1077. this.setData({ keyboardHeight: res.height });
  1078. if (res.height > 0) {
  1079. this.setData({ showMediaSheet: false });
  1080. setTimeout(() => {
  1081. this.scrollToBottom();
  1082. }, 100);
  1083. }
  1084. },
  1085. // 显示多媒体选择面板
  1086. showMediaActionSheet: function() {
  1087. this.setData({
  1088. showMediaSheet: true,
  1089. inputFocus: false
  1090. });
  1091. },
  1092. // 隐藏多媒体选择面板
  1093. hideMediaActionSheet: function() {
  1094. this.setData({ showMediaSheet: false });
  1095. },
  1096. // 选择图片
  1097. chooseImage: function() {
  1098. this.hideMediaActionSheet();
  1099. wx.chooseImage({
  1100. count: 9,
  1101. sizeType: ['compressed'],
  1102. sourceType: ['album'],
  1103. success: (res) => {
  1104. this.uploadImages(res.tempFilePaths);
  1105. },
  1106. fail: () => {
  1107. wx.showToast({
  1108. title: '选择图片失败',
  1109. icon: 'none'
  1110. });
  1111. }
  1112. });
  1113. },
  1114. // 选择视频
  1115. chooseVideo: function() {
  1116. this.hideMediaActionSheet();
  1117. wx.chooseVideo({
  1118. sourceType: ['album'],
  1119. compressed: true,
  1120. maxDuration: 60,
  1121. success: (res) => {
  1122. this.uploadVideo(res.tempFilePath, res.thumbTempFilePath);
  1123. },
  1124. fail: () => {
  1125. wx.showToast({
  1126. title: '选择视频失败',
  1127. icon: 'none'
  1128. });
  1129. }
  1130. });
  1131. },
  1132. // 预览图片
  1133. previewImage: function(e) {
  1134. const url = e.currentTarget.dataset.url;
  1135. wx.previewImage({
  1136. current: url,
  1137. urls: [url]
  1138. });
  1139. },
  1140. // 设置今天日期
  1141. setTodayDate: function() {
  1142. const now = new Date();
  1143. const month = now.getMonth() + 1;
  1144. const date = now.getDate();
  1145. const week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
  1146. this.setData({
  1147. todayDate: `${month}${date}日 星期${week}`
  1148. });
  1149. },
  1150. // 处理消息时间显示
  1151. processMessageTimes: function(messages) {
  1152. if (!messages || messages.length === 0) return [];
  1153. const processedMessages = [];
  1154. let lastShowTime = null;
  1155. for (let i = 0; i < messages.length; i++) {
  1156. const msg = { ...messages[i] };
  1157. if (i === 0) {
  1158. msg.showTime = true;
  1159. lastShowTime = msg.timestamp;
  1160. } else {
  1161. const timeDiffMinutes = (msg.timestamp - lastShowTime) / (1000 * 60);
  1162. msg.showTime = timeDiffMinutes >= this.data.timeInterval;
  1163. if (msg.showTime) lastShowTime = msg.timestamp;
  1164. }
  1165. processedMessages.push(msg);
  1166. }
  1167. if (lastShowTime) {
  1168. this.setData({ lastShowTimeStamp: lastShowTime });
  1169. }
  1170. return processedMessages;
  1171. },
  1172. // 格式化时间
  1173. formatTime: function(timestamp) {
  1174. if (!timestamp) return '';
  1175. const date = new Date(Number(timestamp));
  1176. const hours = date.getHours().toString().padStart(2, '0');
  1177. const minutes = date.getMinutes().toString().padStart(2, '0');
  1178. return `${hours}:${minutes}`;
  1179. },
  1180. // 格式化文件大小
  1181. formatFileSize: function(bytes) {
  1182. if (!bytes || bytes === 0) return '0B';
  1183. const units = ['B', 'KB', 'MB', 'GB'];
  1184. let size = bytes;
  1185. let unitIndex = 0;
  1186. while (size >= 1024 && unitIndex < units.length - 1) {
  1187. size /= 1024;
  1188. unitIndex++;
  1189. }
  1190. return size.toFixed(1) + units[unitIndex];
  1191. },
  1192. // 阻止事件冒泡
  1193. stopPropagation: function() {}
  1194. });