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

641 lines
16 KiB

  1. Page({
  2. data: {
  3. // 专家信息
  4. expertInfo: {
  5. id: 1,
  6. name: '张明专家',
  7. title: '资深畜牧兽医',
  8. expertise: '牛羊疾病防治',
  9. avatar: '/images/avatars/expert1.png',
  10. online: true,
  11. phone: '13800138000'
  12. },
  13. // 用户信息
  14. userInfo: {
  15. id: 1001,
  16. name: '养殖户',
  17. avatar: '/images/avatars/user.png'
  18. },
  19. // 消息列表
  20. messageList: [],
  21. scrollTop: 0,
  22. scrollAnimate: false,
  23. // 输入相关 - 优化
  24. inputValue: '',
  25. inputFocus: false,
  26. // 多媒体
  27. showMediaSheet: false,
  28. // 页面状态
  29. showDateDivider: true,
  30. todayDate: '',
  31. loading: false,
  32. loadingMore: false,
  33. // 滚动相关
  34. isScrolling: false,
  35. lastScrollTop: 0,
  36. // 当前专家ID
  37. currentExpertId: 1,
  38. // 分页相关
  39. page: 1,
  40. pageSize: 20,
  41. hasMore: true,
  42. // 时间显示间隔
  43. timeInterval: 5,
  44. lastShowTimeStamp: 0,
  45. // 键盘高度
  46. keyboardHeight: 0
  47. },
  48. onLoad: function(options) {
  49. this.setTodayDate();
  50. this.loadUserInfo();
  51. if (options.expertId) {
  52. this.setData({ currentExpertId: options.expertId });
  53. this.loadExpertInfo(options.expertId);
  54. } else {
  55. this.loadChatHistory();
  56. }
  57. wx.onKeyboardHeightChange(this.onKeyboardHeightChange.bind(this));
  58. },
  59. onUnload: function() {
  60. wx.offKeyboardHeightChange();
  61. },
  62. // ========== 输入框相关方法 ==========
  63. // 输入处理
  64. onInput: function(e) {
  65. this.setData({
  66. inputValue: e.detail.value
  67. });
  68. },
  69. // 输入框获得焦点
  70. onInputFocus: function() {
  71. this.setData({
  72. inputFocus: true
  73. }, () => {
  74. setTimeout(() => {
  75. this.scrollToBottom();
  76. }, 200);
  77. });
  78. },
  79. // 输入框失去焦点
  80. onInputBlur: function() {
  81. this.setData({
  82. inputFocus: false
  83. });
  84. },
  85. // 清除输入
  86. clearInput: function() {
  87. this.setData({
  88. inputValue: '',
  89. inputFocus: true
  90. });
  91. },
  92. // 发送文本消息
  93. sendTextMessage: function() {
  94. const content = this.data.inputValue.trim();
  95. if (!content) return;
  96. const newMessage = {
  97. id: 'msg-' + Date.now(),
  98. sender: 'user',
  99. type: 'text',
  100. content: content,
  101. timestamp: Date.now(),
  102. status: 'sending'
  103. };
  104. this.addMessageToList(newMessage);
  105. // 清空输入框
  106. this.setData({
  107. inputValue: '',
  108. inputFocus: false
  109. });
  110. // 模拟发送成功
  111. setTimeout(() => {
  112. this.updateMessageStatus(newMessage.id, 'success');
  113. setTimeout(() => {
  114. this.receiveExpertReply();
  115. }, 1000);
  116. }, 500);
  117. },
  118. // 添加消息到列表
  119. addMessageToList: function(message) {
  120. const { messageList } = this.data;
  121. const processedMessage = this.processSingleMessageTime(message, messageList);
  122. messageList.push(processedMessage);
  123. this.setData({ messageList }, () => {
  124. this.scrollToBottom();
  125. });
  126. },
  127. // 更新消息状态
  128. updateMessageStatus: function(messageId, status) {
  129. const { messageList } = this.data;
  130. const index = messageList.findIndex(msg => msg.id === messageId);
  131. if (index !== -1) {
  132. messageList[index].status = status;
  133. this.setData({ messageList });
  134. }
  135. },
  136. // 接收专家回复
  137. receiveExpertReply: function() {
  138. const replies = [
  139. '收到您的消息,让我分析一下您说的情况。',
  140. '建议您提供更多细节,比如发病时间、具体症状等。',
  141. '根据描述,可能是饲料问题引起的,建议调整饲料配方。',
  142. '可以考虑添加一些维生素补充剂,改善食欲问题。',
  143. '最好能提供照片,这样我可以更准确地判断情况。'
  144. ];
  145. const randomReply = replies[Math.floor(Math.random() * replies.length)];
  146. const newMessage = {
  147. id: 'exp-' + Date.now(),
  148. sender: 'expert',
  149. type: 'text',
  150. content: randomReply,
  151. timestamp: Date.now(),
  152. status: 'success'
  153. };
  154. this.addMessageToList(newMessage);
  155. },
  156. // ========== 滚动相关 ==========
  157. // 滚动事件
  158. onScroll: function(e) {
  159. const scrollTop = e.detail.scrollTop;
  160. this.setData({
  161. lastScrollTop: scrollTop,
  162. isScrolling: true
  163. });
  164. clearTimeout(this.scrollTimer);
  165. this.scrollTimer = setTimeout(() => {
  166. this.setData({ isScrolling: false });
  167. }, 200);
  168. if (scrollTop <= 100 && !this.data.loadingMore && this.data.hasMore) {
  169. this.loadMoreMessages();
  170. }
  171. },
  172. // 加载更多消息
  173. loadMoreMessages: function() {
  174. if (this.data.loadingMore || !this.data.hasMore) return;
  175. this.setData({
  176. loadingMore: true,
  177. page: this.data.page + 1
  178. }, () => {
  179. this.loadChatHistory();
  180. });
  181. },
  182. // 滚动到底部
  183. scrollToBottom: function(animate = true) {
  184. if (this.data.isScrolling) return;
  185. this.setData({
  186. scrollAnimate: animate
  187. }, () => {
  188. setTimeout(() => {
  189. this.setData({
  190. scrollTop: 999999
  191. });
  192. }, 50);
  193. });
  194. },
  195. // 键盘高度变化
  196. onKeyboardHeightChange: function(res) {
  197. this.setData({
  198. keyboardHeight: res.height
  199. });
  200. if (res.height > 0) {
  201. this.setData({ showMediaSheet: false });
  202. setTimeout(() => {
  203. this.scrollToBottom();
  204. }, 100);
  205. }
  206. },
  207. // ========== 多媒体相关 ==========
  208. // 显示多媒体选择面板
  209. showMediaActionSheet: function() {
  210. this.setData({
  211. showMediaSheet: true,
  212. inputFocus: false
  213. });
  214. },
  215. // 隐藏多媒体选择面板
  216. hideMediaActionSheet: function() {
  217. this.setData({
  218. showMediaSheet: false
  219. });
  220. },
  221. // 选择图片
  222. chooseImage: function() {
  223. this.hideMediaActionSheet();
  224. wx.chooseImage({
  225. count: 9,
  226. sizeType: ['compressed'],
  227. sourceType: ['album'],
  228. success: (res) => {
  229. this.uploadImages(res.tempFilePaths);
  230. },
  231. fail: (err) => {
  232. wx.showToast({
  233. title: '选择图片失败',
  234. icon: 'none'
  235. });
  236. }
  237. });
  238. },
  239. // 选择视频
  240. chooseVideo: function() {
  241. this.hideMediaActionSheet();
  242. wx.chooseVideo({
  243. sourceType: ['album'],
  244. compressed: true,
  245. maxDuration: 60,
  246. success: (res) => {
  247. this.uploadVideo(res.tempFilePath, res.thumbTempFilePath);
  248. },
  249. fail: (err) => {
  250. wx.showToast({
  251. title: '选择视频失败',
  252. icon: 'none'
  253. });
  254. }
  255. });
  256. },
  257. // 选择文件
  258. chooseFile: function() {
  259. this.hideMediaActionSheet();
  260. wx.chooseMessageFile({
  261. count: 1,
  262. type: 'all',
  263. success: (res) => {
  264. const file = res.tempFiles[0];
  265. this.uploadFile(file.path, file.name, file.size);
  266. },
  267. fail: (err) => {
  268. wx.showToast({
  269. title: '选择文件失败',
  270. icon: 'none'
  271. });
  272. }
  273. });
  274. },
  275. // 上传图片
  276. uploadImages: function(tempFilePaths) {
  277. tempFilePaths.forEach((tempFilePath, index) => {
  278. const fileName = `image_${Date.now()}_${index}.jpg`;
  279. this.uploadFile(tempFilePath, fileName, 0, 'image');
  280. });
  281. },
  282. // 上传视频
  283. uploadVideo: function(tempFilePath, thumbPath) {
  284. const fileName = `video_${Date.now()}.mp4`;
  285. this.uploadFile(tempFilePath, fileName, 0, 'video', thumbPath);
  286. },
  287. // 通用文件上传
  288. uploadFile: function(tempFilePath, fileName, fileSize = 0, type = 'file', thumbPath = '') {
  289. const messageId = 'file-' + Date.now();
  290. const message = {
  291. id: messageId,
  292. sender: 'user',
  293. type: type,
  294. content: tempFilePath,
  295. thumb: thumbPath,
  296. fileName: fileName,
  297. fileSize: fileSize,
  298. extension: fileName.split('.').pop().toLowerCase(),
  299. timestamp: Date.now(),
  300. status: 'uploading',
  301. progress: 0
  302. };
  303. this.addMessageToList(message);
  304. this.simulateUpload(messageId, type);
  305. },
  306. // 模拟上传过程
  307. simulateUpload: function(messageId, type) {
  308. let progress = 0;
  309. const uploadInterval = setInterval(() => {
  310. progress += Math.random() * 20 + 10;
  311. if (progress >= 100) {
  312. progress = 100;
  313. clearInterval(uploadInterval);
  314. setTimeout(() => {
  315. this.updateMessageStatus(messageId, 'success');
  316. const { messageList } = this.data;
  317. const index = messageList.findIndex(msg => msg.id === messageId);
  318. if (index !== -1) {
  319. delete messageList[index].progress;
  320. this.setData({ messageList });
  321. if (type === 'image' || type === 'video') {
  322. setTimeout(() => {
  323. this.receiveMediaReply(type);
  324. }, 800);
  325. }
  326. }
  327. }, 200);
  328. }
  329. const { messageList } = this.data;
  330. const index = messageList.findIndex(msg => msg.id === messageId);
  331. if (index !== -1) {
  332. messageList[index].progress = Math.min(progress, 100);
  333. this.setData({ messageList });
  334. }
  335. }, 100);
  336. },
  337. // 接收媒体回复
  338. receiveMediaReply: function(type) {
  339. const imageReplies = [
  340. '照片收到了,牛的状况看起来确实不太理想。',
  341. '从照片看,饲养环境需要改善一下。',
  342. '图片清晰,我可以更准确地判断问题了。'
  343. ];
  344. const videoReplies = [
  345. '视频看到了,动物的精神状态需要关注。',
  346. '从视频可以观察到更多细节,这很有帮助。',
  347. '视频内容很有价值,让我了解了具体情况。'
  348. ];
  349. const replies = type === 'image' ? imageReplies : videoReplies;
  350. const randomReply = replies[Math.floor(Math.random() * replies.length)];
  351. const newMessage = {
  352. id: 'exp-media-' + Date.now(),
  353. sender: 'expert',
  354. type: 'text',
  355. content: randomReply,
  356. timestamp: Date.now(),
  357. status: 'success'
  358. };
  359. this.addMessageToList(newMessage);
  360. },
  361. // 预览图片
  362. previewImage: function(e) {
  363. const url = e.currentTarget.dataset.url;
  364. wx.previewImage({
  365. current: url,
  366. urls: [url]
  367. });
  368. },
  369. // ========== 数据初始化 ==========
  370. // 设置今天日期
  371. setTodayDate: function() {
  372. const now = new Date();
  373. const month = now.getMonth() + 1;
  374. const date = now.getDate();
  375. const week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
  376. this.setData({
  377. todayDate: `${month}${date}日 星期${week}`
  378. });
  379. },
  380. // 加载用户信息
  381. loadUserInfo: function() {
  382. try {
  383. const userInfo = wx.getStorageSync('userInfo');
  384. if (userInfo) {
  385. this.setData({ userInfo });
  386. }
  387. } catch (e) {
  388. console.error('加载用户信息失败:', e);
  389. }
  390. },
  391. // 加载专家信息
  392. loadExpertInfo: function(expertId) {
  393. wx.showLoading({ title: '加载中...' });
  394. // 模拟网络请求
  395. setTimeout(() => {
  396. this.setData({
  397. expertInfo: {
  398. id: Number(expertId),
  399. name: '张明专家',
  400. title: '资深畜牧兽医',
  401. expertise: '牛羊疾病防治',
  402. avatar: '/images/avatars/expert1.png',
  403. online: true,
  404. phone: '13800138000'
  405. },
  406. loading: false
  407. });
  408. wx.hideLoading();
  409. this.loadChatHistory();
  410. }, 500);
  411. },
  412. // 加载聊天记录
  413. loadChatHistory: function() {
  414. this.setData({ loading: true });
  415. // 模拟历史消息
  416. setTimeout(() => {
  417. const now = Date.now();
  418. const mockMessages = [
  419. {
  420. id: 'msg-1',
  421. sender: 'expert',
  422. type: 'text',
  423. content: '您好,我是张明专家,有什么可以帮您?',
  424. timestamp: now - 10 * 60 * 1000,
  425. status: 'success'
  426. },
  427. {
  428. id: 'msg-2',
  429. sender: 'user',
  430. type: 'text',
  431. content: '您好,我养的牛最近食欲不振,请问是什么原因?',
  432. timestamp: now - 9 * 60 * 1000,
  433. status: 'success'
  434. },
  435. {
  436. id: 'msg-3',
  437. sender: 'expert',
  438. type: 'text',
  439. content: '可能是饲料问题或环境变化引起的,请描述一下具体情况。',
  440. timestamp: now - 7 * 60 * 1000,
  441. status: 'success'
  442. },
  443. {
  444. id: 'msg-4',
  445. sender: 'user',
  446. type: 'text',
  447. content: '具体症状是拉稀,体温偏高,精神状态不好。',
  448. timestamp: now - 2 * 60 * 1000,
  449. status: 'success'
  450. },
  451. {
  452. id: 'msg-5',
  453. sender: 'expert',
  454. type: 'text',
  455. content: '明白了,建议您调整饲料配方,添加一些益生菌。',
  456. timestamp: now - 1 * 60 * 1000,
  457. status: 'success'
  458. }
  459. ];
  460. const processedMessages = this.processMessageTimes(mockMessages);
  461. this.setData({
  462. messageList: processedMessages,
  463. loading: false,
  464. hasMore: false
  465. }, () => {
  466. this.scrollToBottom(true);
  467. });
  468. }, 800);
  469. },
  470. // 处理消息时间显示
  471. processMessageTimes: function(messages) {
  472. if (!messages || messages.length === 0) return [];
  473. const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
  474. const processedMessages = [];
  475. let lastShowTime = null;
  476. for (let i = 0; i < sortedMessages.length; i++) {
  477. const msg = { ...sortedMessages[i] };
  478. if (!msg.timestamp || isNaN(msg.timestamp) || msg.timestamp <= 0) {
  479. msg.timestamp = Date.now() - (sortedMessages.length - i) * 60000;
  480. }
  481. if (i === 0) {
  482. msg.showTime = true;
  483. lastShowTime = msg.timestamp;
  484. } else {
  485. const timeDiffMinutes = (msg.timestamp - lastShowTime) / (1000 * 60);
  486. msg.showTime = timeDiffMinutes >= this.data.timeInterval;
  487. if (msg.showTime) lastShowTime = msg.timestamp;
  488. }
  489. processedMessages.push(msg);
  490. }
  491. if (lastShowTime) {
  492. this.setData({ lastShowTimeStamp: lastShowTime });
  493. }
  494. return processedMessages;
  495. },
  496. // 处理单条消息时间
  497. processSingleMessageTime: function(message, messageList) {
  498. const msg = { ...message };
  499. if (!msg.timestamp || isNaN(msg.timestamp) || msg.timestamp <= 0) {
  500. msg.timestamp = Date.now();
  501. }
  502. if (messageList.length === 0) {
  503. msg.showTime = true;
  504. this.setData({ lastShowTimeStamp: msg.timestamp });
  505. return msg;
  506. }
  507. const lastShowTime = this.data.lastShowTimeStamp;
  508. const timeDiffMinutes = (msg.timestamp - lastShowTime) / (1000 * 60);
  509. msg.showTime = timeDiffMinutes >= this.data.timeInterval;
  510. if (msg.showTime) {
  511. this.setData({ lastShowTimeStamp: msg.timestamp });
  512. }
  513. return msg;
  514. },
  515. // ========== 工具方法 ==========
  516. // 格式化时间
  517. formatTime: function(timestamp) {
  518. if (!timestamp || timestamp <= 0) return '未知时间';
  519. const date = new Date(Number(timestamp));
  520. if (isNaN(date.getTime())) return '未知时间';
  521. const hours = date.getHours().toString().padStart(2, '0');
  522. const minutes = date.getMinutes().toString().padStart(2, '0');
  523. return `${hours}:${minutes}`;
  524. },
  525. // 格式化文件大小
  526. formatFileSize: function(bytes) {
  527. if (!bytes || bytes === 0) return '0B';
  528. const units = ['B', 'KB', 'MB', 'GB'];
  529. let size = bytes;
  530. let unitIndex = 0;
  531. while (size >= 1024 && unitIndex < units.length - 1) {
  532. size /= 1024;
  533. unitIndex++;
  534. }
  535. return size.toFixed(1) + units[unitIndex];
  536. },
  537. // 阻止事件冒泡
  538. stopPropagation: function() {}
  539. });