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

492 lines
12 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. import http from '../../../utils/api'
  2. Page({
  3. data: {
  4. // 当前时间
  5. currentTime: '',
  6. // 聊天消息
  7. messages: [],
  8. // 输入框相关
  9. inputValue: '',
  10. autoFocus: false,
  11. inputHeight: 60,
  12. isKeyboardShow: false,
  13. // 滚动控制
  14. scrollIntoView: 'welcome-message',
  15. scrollAnimation: true,
  16. isUserScrolling: false, // 用户是否正在手动滚动
  17. scrollTimer: null, // 滚动定时器
  18. lastMessageCount: 0, // 上次消息数量
  19. // 症状选择
  20. quickSymptoms: [],
  21. wzsearch: {},
  22. selectedSymptoms: [],
  23. showSymptomSelector: false,
  24. // 状态控制
  25. isAIThinking: false,
  26. isLoading: false,
  27. loadingText: '',
  28. showMoreMenu: false
  29. },
  30. onLoad() {
  31. this.initData();
  32. this.getInquiry();
  33. // 监听键盘高度变化
  34. wx.onKeyboardHeightChange(this.onKeyboardHeightChange.bind(this));
  35. },
  36. onUnload() {
  37. // 页面卸载时移除监听
  38. wx.offKeyboardHeightChange(this.onKeyboardHeightChange);
  39. // 清除定时器
  40. if (this.data.scrollTimer) {
  41. clearTimeout(this.data.scrollTimer);
  42. }
  43. },
  44. // 监听滚动事件
  45. onScroll(e) {
  46. // 检测用户是否在手动滚动
  47. const scrollTop = e.detail.scrollTop;
  48. // 获取滚动容器和底部元素的位置
  49. const query = wx.createSelectorQuery();
  50. query.select('#scrollBottom').boundingClientRect();
  51. query.select('#chatScroll').boundingClientRect();
  52. query.exec((res) => {
  53. if (res[0] && res[1]) {
  54. const bottomPosition = res[0].top - res[1].top + res[1].height;
  55. // 如果滚动位置不在底部(偏差超过50px),认为是用户手动滚动
  56. if (Math.abs(scrollTop - bottomPosition) > 100) {
  57. if (!this.data.isUserScrolling) {
  58. this.setData({
  59. isUserScrolling: true
  60. });
  61. }
  62. // 清除之前的定时器
  63. if (this.data.scrollTimer) {
  64. clearTimeout(this.data.scrollTimer);
  65. }
  66. // 5秒后重置用户滚动状态
  67. const timer = setTimeout(() => {
  68. this.setData({
  69. isUserScrolling: false
  70. });
  71. // 重置后检查是否需要滚动到底部
  72. this.checkAndScrollToBottom();
  73. }, 5000);
  74. this.setData({ scrollTimer: timer });
  75. }
  76. }
  77. });
  78. },
  79. // 滚动到底部事件
  80. onScrollToLower() {
  81. // 用户滚动到底部时,重置滚动状态
  82. this.setData({
  83. isUserScrolling: false
  84. });
  85. },
  86. // 键盘高度变化监听
  87. onKeyboardHeightChange(res) {
  88. if (res.height > 0) {
  89. // 键盘弹起
  90. this.setData({
  91. isKeyboardShow: true
  92. });
  93. // 强制滚动到底部
  94. this.scrollToBottom(true);
  95. } else {
  96. // 键盘收起
  97. this.setData({
  98. isKeyboardShow: false
  99. });
  100. }
  101. },
  102. // 检查并滚动到底部
  103. checkAndScrollToBottom() {
  104. // 如果用户没有手动滚动,则滚动到底部
  105. if (!this.data.isUserScrolling) {
  106. this.scrollToBottom(true);
  107. }
  108. },
  109. // AI问诊快捷字列表
  110. getInquiry() {
  111. http.inquiry({
  112. data: {},
  113. success: res => {
  114. console.log('快捷症状列表', res);
  115. this.setData({
  116. quickSymptoms: res.rows
  117. });
  118. }
  119. });
  120. },
  121. onShow() {
  122. this.updateCurrentTime();
  123. // 延迟设置焦点,避免影响滚动
  124. setTimeout(() => {
  125. this.setData({
  126. autoFocus: true
  127. });
  128. }, 300);
  129. },
  130. // 初始化数据
  131. initData() {
  132. // 设置当前时间
  133. this.updateCurrentTime();
  134. // 定时更新当前时间
  135. setInterval(() => {
  136. this.updateCurrentTime();
  137. }, 60000);
  138. // 初始化消息数量
  139. this.setData({
  140. lastMessageCount: this.data.messages.length
  141. });
  142. },
  143. // 更新当前时间
  144. updateCurrentTime() {
  145. const now = new Date();
  146. const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  147. this.setData({
  148. currentTime: timeString
  149. });
  150. },
  151. // 滚动到底部
  152. scrollToBottom(force = false) {
  153. // 如果用户正在手动滚动且不是强制滚动,则不滚动
  154. if (this.data.isUserScrolling && !force) {
  155. console.log('用户正在手动滚动,跳过自动滚动');
  156. return;
  157. }
  158. // 获取最新消息的ID
  159. let targetId = 'scrollBottom';
  160. // 优先滚动到最新的消息
  161. if (this.data.messages.length > 0) {
  162. targetId = `msg-${this.data.messages.length - 1}`;
  163. } else {
  164. targetId = 'welcome-message';
  165. }
  166. console.log('滚动到:', targetId);
  167. // 先重置scrollIntoView,确保能触发滚动
  168. this.setData({
  169. scrollIntoView: '',
  170. scrollAnimation: true
  171. }, () => {
  172. // 延迟一下再设置滚动目标,确保视图更新
  173. setTimeout(() => {
  174. this.setData({
  175. scrollIntoView: targetId,
  176. scrollAnimation: true
  177. });
  178. }, 50);
  179. });
  180. // 多重保障滚动
  181. setTimeout(() => {
  182. if (!this.data.isUserScrolling || force) {
  183. this.setData({
  184. scrollIntoView: targetId
  185. });
  186. }
  187. }, 150);
  188. setTimeout(() => {
  189. if (!this.data.isUserScrolling || force) {
  190. this.setData({
  191. scrollIntoView: targetId
  192. });
  193. }
  194. }, 300);
  195. },
  196. // 输入框行数变化
  197. onInputLineChange(e) {
  198. // 输入框高度变化时,确保滚动到底部
  199. if (this.data.isKeyboardShow) {
  200. this.scrollToBottom();
  201. }
  202. },
  203. // 输入框聚焦
  204. onInputFocus(e) {
  205. this.setData({
  206. autoFocus: true,
  207. isKeyboardShow: true
  208. });
  209. // 延迟滚动到底部,等待键盘完全弹起
  210. setTimeout(() => {
  211. this.scrollToBottom(true);
  212. }, 300);
  213. },
  214. // 输入框失焦
  215. onInputBlur(e) {
  216. this.setData({
  217. autoFocus: false,
  218. isKeyboardShow: false
  219. });
  220. },
  221. // 输入框变化
  222. onInput(e) {
  223. this.setData({
  224. inputValue: e.detail.value
  225. });
  226. },
  227. // 发送消息
  228. sendMessage() {
  229. const message = this.data.inputValue.trim();
  230. if (!message) return;
  231. // 添加用户消息
  232. const userMessage = {
  233. id: Date.now(),
  234. type: 'user',
  235. content: message,
  236. time: this.getCurrentTime()
  237. };
  238. // 更新消息列表
  239. const newMessages = [...this.data.messages, userMessage];
  240. this.setData({
  241. inputValue: '',
  242. autoFocus: true,
  243. inputHeight: 60,
  244. messages: newMessages,
  245. isUserScrolling: false, // 发送消息时重置用户滚动状态
  246. lastMessageCount: newMessages.length
  247. });
  248. // 立即滚动到底部
  249. this.scrollToBottom(true);
  250. // 显示AI思考状态
  251. this.setData({
  252. isAIThinking: true
  253. });
  254. // 滚动到底部(显示思考状态后)
  255. setTimeout(() => {
  256. this.scrollToBottom(true);
  257. }, 100);
  258. // 模拟AI思考时间
  259. setTimeout(() => {
  260. this.generateAIResponse(message);
  261. }, 1500 + Math.random() * 1000);
  262. },
  263. // 获取当前时间
  264. getCurrentTime() {
  265. const now = new Date();
  266. return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  267. },
  268. // 生成AI响应
  269. generateAIResponse(userMessage) {
  270. console.log('发送消息:', userMessage);
  271. http.search({
  272. data: {
  273. keyword: userMessage
  274. },
  275. success: res => {
  276. console.log('AI响应:', res);
  277. let aiMessage;
  278. if (res.rows && res.rows.length > 0) {
  279. const wzsearch = res.rows[0];
  280. aiMessage = {
  281. id: Date.now() + 1,
  282. type: 'assistant',
  283. content: '根据您的描述,' + (wzsearch.title || '已收到您的症状描述'),
  284. diagnosis: wzsearch,
  285. time: this.getCurrentTime()
  286. };
  287. } else {
  288. aiMessage = {
  289. id: Date.now() + 1,
  290. type: 'assistant',
  291. content: '已收到您的症状描述',
  292. diagnosis: {
  293. possibleDiseases: '暂无匹配诊断',
  294. severityLevel: '未知',
  295. suggestions: '建议您提供更详细的症状描述,或直接咨询专业兽医'
  296. },
  297. time: this.getCurrentTime()
  298. };
  299. }
  300. // 添加AI消息
  301. const newMessages = [...this.data.messages, aiMessage];
  302. this.setData({
  303. messages: newMessages,
  304. isAIThinking: false,
  305. isUserScrolling: false, // 收到新消息时强制重置用户滚动状态
  306. lastMessageCount: newMessages.length
  307. }, () => {
  308. // 消息添加后立即滚动到底部
  309. console.log('AI消息已添加,滚动到底部');
  310. this.scrollToBottom(true);
  311. // 多重保险滚动
  312. setTimeout(() => {
  313. this.scrollToBottom(true);
  314. }, 100);
  315. setTimeout(() => {
  316. this.scrollToBottom(true);
  317. }, 300);
  318. setTimeout(() => {
  319. this.scrollToBottom(true);
  320. }, 500);
  321. });
  322. },
  323. fail: err => {
  324. console.error('API请求失败:', err);
  325. const aiMessage = {
  326. id: Date.now() + 1,
  327. type: 'assistant',
  328. content: '已收到您的症状描述',
  329. diagnosis: {
  330. possibleDiseases: '网络请求失败',
  331. severityLevel: '未知',
  332. suggestions: '请检查网络连接后重试,或直接联系兽医'
  333. },
  334. time: this.getCurrentTime()
  335. };
  336. const newMessages = [...this.data.messages, aiMessage];
  337. this.setData({
  338. messages: newMessages,
  339. isAIThinking: false,
  340. isUserScrolling: false,
  341. lastMessageCount: newMessages.length
  342. }, () => {
  343. this.scrollToBottom(true);
  344. });
  345. },
  346. complete: () => {
  347. if (this.data.isAIThinking) {
  348. this.setData({
  349. isAIThinking: false
  350. });
  351. }
  352. }
  353. });
  354. },
  355. // 选择快捷症状
  356. selectQuickSymptom(e) {
  357. const symptom = e.currentTarget.dataset.symptom.keywords;
  358. this.setData({
  359. inputValue: symptom
  360. });
  361. this.sendMessage();
  362. },
  363. // 返回
  364. goBack() {
  365. wx.navigateBack();
  366. },
  367. // 显示更多菜单
  368. showMoreMenu() {
  369. this.setData({
  370. showMoreMenu: true
  371. });
  372. },
  373. // 关闭更多菜单
  374. closeMoreMenu() {
  375. this.setData({
  376. showMoreMenu: false
  377. });
  378. },
  379. // 阻止事件冒泡
  380. stopPropagation() {},
  381. // 清空记录
  382. clearHistory() {
  383. wx.showModal({
  384. title: '提示',
  385. content: '确定要清空所有聊天记录吗?',
  386. success: (res) => {
  387. if (res.confirm) {
  388. this.setData({
  389. messages: [],
  390. selectedSymptoms: [],
  391. isUserScrolling: false,
  392. lastMessageCount: 0
  393. }, () => {
  394. // 清空后滚动到欢迎消息
  395. this.setData({
  396. scrollIntoView: 'welcome-message'
  397. });
  398. });
  399. this.closeMoreMenu();
  400. }
  401. }
  402. });
  403. },
  404. // 导出记录
  405. exportChat() {
  406. wx.showToast({
  407. title: '记录已保存到本地',
  408. icon: 'success'
  409. });
  410. this.closeMoreMenu();
  411. },
  412. // 联系兽医
  413. contactDoctor() {
  414. wx.showModal({
  415. title: '联系兽医',
  416. content: '确定要拨打兽医热线吗?',
  417. success: (res) => {
  418. if (res.confirm) {
  419. wx.makePhoneCall({
  420. phoneNumber: '400-123-4567'
  421. });
  422. }
  423. }
  424. });
  425. this.closeMoreMenu();
  426. },
  427. // 显示使用说明
  428. showInstructions() {
  429. wx.showModal({
  430. title: '使用说明',
  431. content: '1. 描述您或牲畜的症状\n2. AI助手会分析并提供建议\n3. 可使用快捷症状选择\n4. 诊断结果仅供参考,请及时咨询专业兽医',
  432. showCancel: false
  433. });
  434. this.closeMoreMenu();
  435. }
  436. });