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

752 lines
18 KiB

  1. // 获取应用实例
  2. const app = getApp()
  3. Page({
  4. data: {
  5. // 专家信息
  6. expertInfo: {
  7. id: 1,
  8. name: '张明专家',
  9. title: '资深畜牧兽医',
  10. expertise: '牛羊疾病防治',
  11. avatar: '/images/avatars/expert1.png',
  12. online: true,
  13. phone: '13800138000'
  14. },
  15. // 用户信息
  16. userInfo: {
  17. id: 1001,
  18. name: '养殖户',
  19. avatar: '/images/avatars/user.png'
  20. },
  21. // 消息列表
  22. messageList: [],
  23. scrollToView: '',
  24. // 输入相关
  25. inputValue: '',
  26. inputFocus: false,
  27. inputMode: 'keyboard', // keyboard or voice
  28. inputPlaceholder: '输入消息...',
  29. // 多媒体
  30. showMediaSheet: false,
  31. // 录音相关
  32. isRecording: false,
  33. recordingTime: 0,
  34. recordingTip: '上滑取消录音',
  35. recordingTimer: null,
  36. recordManager: null,
  37. // 页面状态
  38. isFirstLoad: true,
  39. showDateDivider: true,
  40. todayDate: '',
  41. // 消息ID计数器
  42. messageId: 1000,
  43. // 存储键名
  44. storageKey: 'consult_messages_'
  45. },
  46. onLoad: function(options) {
  47. // 初始化录音管理器
  48. this.initRecordManager()
  49. // 获取今天日期
  50. this.setTodayDate()
  51. // 加载用户信息
  52. this.loadUserInfo()
  53. // 加载专家信息
  54. if (options.expertId) {
  55. this.loadExpertInfo(options.expertId)
  56. }
  57. // 加载聊天记录
  58. this.loadChatHistory()
  59. // 设置键盘监听
  60. wx.onKeyboardHeightChange(this.onKeyboardHeightChange)
  61. // 模拟首次进入时的欢迎消息
  62. setTimeout(() => {
  63. this.setData({ isFirstLoad: false })
  64. }, 2000)
  65. },
  66. onUnload: function() {
  67. // 清理定时器
  68. if (this.data.recordingTimer) {
  69. clearInterval(this.data.recordingTimer)
  70. }
  71. // 移除监听器
  72. wx.offKeyboardHeightChange()
  73. // 保存聊天记录
  74. this.saveChatHistory()
  75. },
  76. onShow: function() {
  77. // 页面显示时自动滚动到底部
  78. setTimeout(() => {
  79. this.scrollToBottom()
  80. }, 300)
  81. },
  82. // 初始化录音管理器
  83. initRecordManager: function() {
  84. this.recordManager = wx.getRecorderManager()
  85. this.recordManager.onStart(() => {
  86. console.log('录音开始')
  87. })
  88. this.recordManager.onStop((res) => {
  89. const { tempFilePath, duration } = res
  90. if (tempFilePath) {
  91. this.sendAudioMessage(tempFilePath, Math.floor(duration / 1000))
  92. }
  93. })
  94. this.recordManager.onError((err) => {
  95. console.error('录音失败:', err)
  96. wx.showToast({
  97. title: '录音失败',
  98. icon: 'none'
  99. })
  100. this.setData({ isRecording: false })
  101. })
  102. },
  103. // 设置今天日期
  104. setTodayDate: function() {
  105. const now = new Date()
  106. const month = now.getMonth() + 1
  107. const date = now.getDate()
  108. const week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()]
  109. this.setData({
  110. todayDate: `${month}${date}日 星期${week}`
  111. })
  112. },
  113. // 加载用户信息
  114. loadUserInfo: function() {
  115. const userInfo = wx.getStorageSync('userInfo') || this.data.userInfo
  116. this.setData({ userInfo })
  117. },
  118. // 加载专家信息
  119. loadExpertInfo: function(expertId) {
  120. // 这里应该是API请求,暂时使用模拟数据
  121. wx.showLoading({ title: '加载中...' })
  122. setTimeout(() => {
  123. const expertInfo = {
  124. id: expertId,
  125. name: ['张明', '李华', '王强', '赵刚'][expertId - 1] + '专家',
  126. title: '资深畜牧兽医',
  127. expertise: '牛羊疾病防治',
  128. avatar: `/images/avatars/expert${expertId}.png`,
  129. online: Math.random() > 0.3,
  130. phone: '138' + Math.floor(Math.random() * 100000000).toString().padStart(8, '0')
  131. }
  132. this.setData({
  133. expertInfo,
  134. storageKey: `consult_messages_${expertId}_${this.data.userInfo.id}`
  135. })
  136. wx.hideLoading()
  137. }, 500)
  138. },
  139. // 加载聊天记录
  140. loadChatHistory: function() {
  141. const storageKey = this.data.storageKey
  142. const savedMessages = wx.getStorageSync(storageKey) || []
  143. if (savedMessages.length > 0) {
  144. // 处理消息时间显示
  145. const processedMessages = this.processMessageTimes(savedMessages)
  146. this.setData({
  147. messageList: processedMessages,
  148. isFirstLoad: false
  149. })
  150. // 滚动到底部
  151. setTimeout(() => {
  152. this.scrollToBottom()
  153. }, 200)
  154. } else {
  155. // 初始化第一条欢迎消息
  156. this.initWelcomeMessage()
  157. }
  158. },
  159. // 处理消息时间显示
  160. processMessageTimes: function(messages) {
  161. if (!messages || messages.length === 0) return messages
  162. let lastTimestamp = 0
  163. return messages.map((msg, index) => {
  164. const currentTimestamp = msg.timestamp
  165. // 如果两条消息间隔超过5分钟,显示时间
  166. const timeDiff = currentTimestamp - lastTimestamp
  167. msg.showTime = timeDiff > 5 * 60 * 1000 || index === 0
  168. if (msg.showTime) {
  169. lastTimestamp = currentTimestamp
  170. }
  171. return msg
  172. })
  173. },
  174. // 初始化欢迎消息
  175. initWelcomeMessage: function() {
  176. const welcomeMessage = {
  177. id: 'welcome-' + Date.now(),
  178. sender: 'expert',
  179. type: 'text',
  180. content: '您好,我是' + this.data.expertInfo.name + ',有什么可以帮您?',
  181. timestamp: Date.now() - 60000,
  182. showTime: true
  183. }
  184. this.setData({
  185. messageList: [welcomeMessage]
  186. })
  187. },
  188. // 保存聊天记录
  189. saveChatHistory: function() {
  190. const { storageKey, messageList } = this.data
  191. if (messageList.length > 0) {
  192. try {
  193. wx.setStorageSync(storageKey, messageList)
  194. } catch (e) {
  195. console.error('保存聊天记录失败:', e)
  196. }
  197. }
  198. },
  199. // 返回上一页
  200. goBack: function() {
  201. wx.navigateBack()
  202. },
  203. // 打电话
  204. makePhoneCall: function() {
  205. const phone = this.data.expertInfo.phone
  206. wx.makePhoneCall({
  207. phoneNumber: phone,
  208. fail: () => {
  209. wx.showToast({
  210. title: '拨打失败',
  211. icon: 'none'
  212. })
  213. }
  214. })
  215. },
  216. // 输入处理
  217. onInput: function(e) {
  218. this.setData({
  219. inputValue: e.detail.value
  220. })
  221. },
  222. // 发送文本消息
  223. sendTextMessage: function() {
  224. const content = this.data.inputValue.trim()
  225. if (!content) return
  226. const newMessage = {
  227. id: 'msg-' + (++this.data.messageId),
  228. sender: 'user',
  229. type: 'text',
  230. content: content,
  231. timestamp: Date.now(),
  232. status: 'sending'
  233. }
  234. // 添加到消息列表
  235. this.addMessageToList(newMessage)
  236. // 清空输入框
  237. this.setData({
  238. inputValue: '',
  239. inputFocus: false
  240. })
  241. // 模拟发送成功
  242. setTimeout(() => {
  243. this.updateMessageStatus(newMessage.id, 'success')
  244. // 模拟专家回复
  245. setTimeout(() => {
  246. this.receiveExpertReply()
  247. }, 1000)
  248. }, 500)
  249. },
  250. // 添加消息到列表
  251. addMessageToList: function(message) {
  252. const { messageList } = this.data
  253. // 确定是否需要显示时间
  254. const lastMessage = messageList[messageList.length - 1]
  255. const timeDiff = lastMessage ? message.timestamp - lastMessage.timestamp : 0
  256. message.showTime = timeDiff > 5 * 60 * 1000 || !lastMessage
  257. messageList.push(message)
  258. this.setData({
  259. messageList,
  260. scrollToView: 'msg-' + message.id
  261. })
  262. },
  263. // 更新消息状态
  264. updateMessageStatus: function(messageId, status) {
  265. const { messageList } = this.data
  266. const index = messageList.findIndex(msg => msg.id === messageId)
  267. if (index !== -1) {
  268. messageList[index].status = status
  269. this.setData({ messageList })
  270. }
  271. },
  272. // 接收专家回复
  273. receiveExpertReply: function() {
  274. const replies = [
  275. '收到您的消息,让我分析一下您说的情况。',
  276. '建议您提供更多细节,比如发病时间、具体症状等。',
  277. '根据描述,可能是饲料问题引起的,建议调整饲料配方。',
  278. '可以考虑添加一些维生素补充剂,改善食欲问题。',
  279. '最好能提供照片,这样我可以更准确地判断情况。'
  280. ]
  281. const randomReply = replies[Math.floor(Math.random() * replies.length)]
  282. const newMessage = {
  283. id: 'exp-' + Date.now(),
  284. sender: 'expert',
  285. type: 'text',
  286. content: randomReply,
  287. timestamp: Date.now(),
  288. showTime: false
  289. }
  290. this.addMessageToList(newMessage)
  291. },
  292. // 切换输入模式
  293. switchInputMode: function() {
  294. const newMode = this.data.inputMode === 'keyboard' ? 'voice' : 'keyboard'
  295. const placeholder = newMode === 'voice' ? '按住说话' : '输入消息...'
  296. this.setData({
  297. inputMode: newMode,
  298. inputPlaceholder: placeholder,
  299. inputFocus: newMode === 'keyboard'
  300. })
  301. },
  302. // 显示多媒体选择面板
  303. showMediaActionSheet: function() {
  304. this.setData({
  305. showMediaSheet: true,
  306. inputFocus: false
  307. })
  308. },
  309. // 隐藏多媒体选择面板
  310. hideMediaActionSheet: function() {
  311. this.setData({
  312. showMediaSheet: false
  313. })
  314. },
  315. // 选择图片
  316. chooseImage: function() {
  317. this.hideMediaActionSheet()
  318. wx.chooseImage({
  319. count: 9,
  320. sizeType: ['compressed'],
  321. sourceType: ['album'],
  322. success: (res) => {
  323. this.uploadImages(res.tempFilePaths)
  324. }
  325. })
  326. },
  327. // 拍照
  328. takePhoto: function() {
  329. this.hideMediaActionSheet()
  330. wx.chooseImage({
  331. count: 1,
  332. sizeType: ['compressed'],
  333. sourceType: ['camera'],
  334. success: (res) => {
  335. this.uploadImages(res.tempFilePaths)
  336. }
  337. })
  338. },
  339. // 选择视频
  340. chooseVideo: function() {
  341. this.hideMediaActionSheet()
  342. wx.chooseVideo({
  343. sourceType: ['album'],
  344. compressed: true,
  345. maxDuration: 60,
  346. success: (res) => {
  347. this.uploadVideo(res.tempFilePath, res.thumbTempFilePath)
  348. }
  349. })
  350. },
  351. // 录制语音
  352. recordAudio: function() {
  353. this.hideMediaActionSheet()
  354. this.startVoiceRecord()
  355. },
  356. // 选择文件
  357. chooseFile: function() {
  358. this.hideMediaActionSheet()
  359. wx.chooseMessageFile({
  360. count: 1,
  361. type: 'all',
  362. success: (res) => {
  363. const file = res.tempFiles[0]
  364. this.uploadFile(file.path, file.name, file.size)
  365. }
  366. })
  367. },
  368. // 上传图片
  369. uploadImages: function(tempFilePaths) {
  370. tempFilePaths.forEach((tempFilePath, index) => {
  371. const fileName = 'image_' + Date.now() + '_' + index + '.jpg'
  372. this.uploadFile(tempFilePath, fileName, 0, 'image')
  373. })
  374. },
  375. // 上传视频
  376. uploadVideo: function(tempFilePath, thumbPath) {
  377. const fileName = 'video_' + Date.now() + '.mp4'
  378. this.uploadFile(tempFilePath, fileName, 0, 'video', thumbPath)
  379. },
  380. // 通用文件上传
  381. uploadFile: function(tempFilePath, fileName, fileSize = 0, type = 'file', thumbPath = '') {
  382. // 获取文件扩展名
  383. const extension = fileName.split('.').pop().toLowerCase()
  384. // 创建消息
  385. const messageId = 'file-' + Date.now()
  386. const message = {
  387. id: messageId,
  388. sender: 'user',
  389. type: type,
  390. content: tempFilePath,
  391. thumb: thumbPath,
  392. fileName: fileName,
  393. fileSize: fileSize,
  394. extension: extension,
  395. timestamp: Date.now(),
  396. status: 'uploading',
  397. progress: 0
  398. }
  399. this.addMessageToList(message)
  400. // 模拟上传过程
  401. let progress = 0
  402. const uploadInterval = setInterval(() => {
  403. progress += Math.random() * 20 + 10
  404. if (progress >= 100) {
  405. progress = 100
  406. clearInterval(uploadInterval)
  407. setTimeout(() => {
  408. this.updateMessageStatus(messageId, 'success')
  409. // 清除进度信息
  410. const { messageList } = this.data
  411. const index = messageList.findIndex(msg => msg.id === messageId)
  412. if (index !== -1) {
  413. delete messageList[index].progress
  414. this.setData({ messageList })
  415. // 模拟专家回复
  416. if (type === 'image' || type === 'video') {
  417. setTimeout(() => {
  418. this.receiveMediaReply(type)
  419. }, 800)
  420. }
  421. }
  422. }, 200)
  423. }
  424. // 更新进度
  425. const { messageList } = this.data
  426. const index = messageList.findIndex(msg => msg.id === messageId)
  427. if (index !== -1) {
  428. messageList[index].progress = Math.min(progress, 100)
  429. this.setData({ messageList })
  430. }
  431. }, 100)
  432. },
  433. // 接收媒体回复
  434. receiveMediaReply: function(type) {
  435. const imageReplies = [
  436. '照片收到了,牛的状况看起来确实不太理想。',
  437. '从照片看,饲养环境需要改善一下。',
  438. '图片清晰,我可以更准确地判断问题了。'
  439. ]
  440. const videoReplies = [
  441. '视频看到了,动物的精神状态需要关注。',
  442. '从视频可以观察到更多细节,这很有帮助。',
  443. '视频内容很有价值,让我了解了具体情况。'
  444. ]
  445. const replies = type === 'image' ? imageReplies : videoReplies
  446. const randomReply = replies[Math.floor(Math.random() * replies.length)]
  447. const newMessage = {
  448. id: 'exp-media-' + Date.now(),
  449. sender: 'expert',
  450. type: 'text',
  451. content: randomReply,
  452. timestamp: Date.now(),
  453. showTime: false
  454. }
  455. this.addMessageToList(newMessage)
  456. },
  457. // 开始语音录制
  458. startVoiceRecord: function(e) {
  459. this.setData({
  460. isRecording: true,
  461. recordingTime: 0,
  462. recordingTip: '上滑取消录音'
  463. })
  464. // 开始录音
  465. this.recordManager.start({
  466. duration: 60000, // 最长60秒
  467. sampleRate: 44100,
  468. numberOfChannels: 1,
  469. encodeBitRate: 192000,
  470. format: 'mp3'
  471. })
  472. // 开始计时
  473. const timer = setInterval(() => {
  474. const time = this.data.recordingTime + 1
  475. this.setData({ recordingTime: time })
  476. }, 1000)
  477. this.setData({ recordingTimer: timer })
  478. },
  479. // 结束语音录制
  480. endVoiceRecord: function() {
  481. if (this.data.isRecording) {
  482. this.recordManager.stop()
  483. if (this.data.recordingTimer) {
  484. clearInterval(this.data.recordingTimer)
  485. }
  486. this.setData({
  487. isRecording: false,
  488. recordingTime: 0
  489. })
  490. }
  491. },
  492. // 发送语音消息
  493. sendAudioMessage: function(tempFilePath, duration) {
  494. const message = {
  495. id: 'audio-' + Date.now(),
  496. sender: 'user',
  497. type: 'audio',
  498. content: tempFilePath,
  499. duration: duration,
  500. timestamp: Date.now(),
  501. status: 'sending'
  502. }
  503. this.addMessageToList(message)
  504. // 模拟发送成功
  505. setTimeout(() => {
  506. this.updateMessageStatus(message.id, 'success')
  507. // 模拟专家回复
  508. setTimeout(() => {
  509. const reply = {
  510. id: 'exp-audio-' + Date.now(),
  511. sender: 'expert',
  512. type: 'text',
  513. content: '语音收到了,我会仔细听取分析。',
  514. timestamp: Date.now(),
  515. showTime: false
  516. }
  517. this.addMessageToList(reply)
  518. }, 1500)
  519. }, 800)
  520. },
  521. // 预览图片
  522. previewImage: function(e) {
  523. const url = e.currentTarget.dataset.url
  524. wx.previewImage({
  525. current: url,
  526. urls: [url]
  527. })
  528. },
  529. // 下载文件
  530. downloadFile: function(e) {
  531. const url = e.currentTarget.dataset.url
  532. wx.showLoading({ title: '下载中...' })
  533. wx.downloadFile({
  534. url: url,
  535. success: (res) => {
  536. wx.hideLoading()
  537. wx.showToast({
  538. title: '下载成功',
  539. icon: 'success'
  540. })
  541. // 保存到相册(如果是图片)
  542. if (url.match(/\.(jpg|jpeg|png|gif)$/i)) {
  543. wx.saveImageToPhotosAlbum({
  544. filePath: res.tempFilePath,
  545. success: () => {
  546. wx.showToast({
  547. title: '已保存到相册',
  548. icon: 'success'
  549. })
  550. }
  551. })
  552. }
  553. },
  554. fail: () => {
  555. wx.hideLoading()
  556. wx.showToast({
  557. title: '下载失败',
  558. icon: 'none'
  559. })
  560. }
  561. })
  562. },
  563. // 键盘高度变化
  564. onKeyboardHeightChange: function(res) {
  565. if (res.height > 0) {
  566. // 键盘弹出时隐藏多媒体面板
  567. this.setData({ showMediaSheet: false })
  568. // 滚动到底部
  569. setTimeout(() => {
  570. this.scrollToBottom()
  571. }, 100)
  572. }
  573. },
  574. // 滚动到底部
  575. scrollToBottom: function() {
  576. if (this.data.messageList.length > 0) {
  577. const lastMessage = this.data.messageList[this.data.messageList.length - 1]
  578. this.setData({
  579. scrollToView: lastMessage.id
  580. })
  581. }
  582. },
  583. // 格式化时间(微信样式)
  584. formatTime: function(timestamp) {
  585. const now = new Date()
  586. const date = new Date(timestamp)
  587. const diff = now - date
  588. // 今天
  589. if (date.toDateString() === now.toDateString()) {
  590. return this.formatMessageTime(timestamp)
  591. }
  592. // 昨天
  593. const yesterday = new Date(now)
  594. yesterday.setDate(yesterday.getDate() - 1)
  595. if (date.toDateString() === yesterday.toDateString()) {
  596. return '昨天 ' + this.formatMessageTime(timestamp)
  597. }
  598. // 一周内
  599. if (diff < 7 * 24 * 60 * 60 * 1000) {
  600. const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  601. return weekDays[date.getDay()] + ' ' + this.formatMessageTime(timestamp)
  602. }
  603. // 今年内
  604. if (date.getFullYear() === now.getFullYear()) {
  605. return `${date.getMonth() + 1}${date.getDate()}${this.formatMessageTime(timestamp)}`
  606. }
  607. // 更早
  608. return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${this.formatMessageTime(timestamp)}`
  609. },
  610. // 格式化消息时间(HH:mm)
  611. formatMessageTime: function(timestamp) {
  612. const date = new Date(timestamp)
  613. const hours = date.getHours().toString().padStart(2, '0')
  614. const minutes = date.getMinutes().toString().padStart(2, '0')
  615. return `${hours}:${minutes}`
  616. },
  617. // 格式化文件大小
  618. formatFileSize: function(bytes) {
  619. if (bytes === 0) return '未知大小'
  620. const units = ['B', 'KB', 'MB', 'GB']
  621. let size = bytes
  622. let unitIndex = 0
  623. while (size >= 1024 && unitIndex < units.length - 1) {
  624. size /= 1024
  625. unitIndex++
  626. }
  627. return size.toFixed(1) + units[unitIndex]
  628. },
  629. // 阻止事件冒泡
  630. stopPropagation: function() {
  631. // 空函数,仅用于阻止事件冒泡
  632. }
  633. })