与牧同行-兽医端小程序
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.

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