index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <template>
  2. <view class="container">
  3. <!-- 聊天记录区域 -->
  4. <scroll-view
  5. class="chat-container"
  6. scroll-y
  7. :scroll-top="scrollTop"
  8. :scroll-with-animation="true"
  9. @scroll="onScroll"
  10. :style="{
  11. height: `calc(100vh - ${inputHeight}px)`,
  12. marginTop: '0'
  13. }"
  14. >
  15. <view class="chat-list">
  16. <view
  17. v-for="(message, index) in chatMessages"
  18. :key="index"
  19. class="message-item"
  20. :class="{ 'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user' }"
  21. >
  22. <!-- AI消息 -->
  23. <template v-if="message.sender === 'ai'">
  24. <view class="avatar-container">
  25. <image class="avatar" src="/static/icons/ai.png" mode="aspectFill"></image>
  26. </view>
  27. <view class="message-content">
  28. <view class="message-bubble ai-bubble" :class="{'typing': message.isTyping, 'welcome': message.isWelcome || index === 0}">
  29. <view v-if="message.isTyping" class="typing-indicator">
  30. <view class="typing-dot"></view>
  31. <view class="typing-dot"></view>
  32. <view class="typing-dot"></view>
  33. </view>
  34. <text v-else class="message-text" :class="{'highlight': containsKeywords(message.content)}">{{ message.content }}</text>
  35. </view>
  36. <text class="message-time">{{ message.time }}</text>
  37. </view>
  38. </template>
  39. <!-- 用户消息 -->
  40. <template v-else>
  41. <view class="message-content user-content">
  42. <text class="message-time">{{ message.time }}</text>
  43. <view class="message-bubble user-bubble">
  44. <text class="message-text">{{ message.content }}</text>
  45. </view>
  46. </view>
  47. <view class="avatar-container">
  48. <image class="avatar" src="/static/images/user-avatar.svg" mode="aspectFill"></image>
  49. </view>
  50. </template>
  51. </view>
  52. </view>
  53. </scroll-view>
  54. <!-- 底部输入区 -->
  55. <view class="input-container" :style="{ paddingBottom: `${isIOS ? safeAreaBottom : 20}rpx` }">
  56. <!-- 问题建议区 -->
  57. <scroll-view
  58. v-if="chatMessages.length <= 3 && !inputMessage"
  59. class="suggested-questions"
  60. scroll-x
  61. >
  62. <view
  63. v-for="(question, index) in suggestedQuestions"
  64. :key="index"
  65. class="question-chip"
  66. @click="useQuestion(question)"
  67. >
  68. <text>{{ question }}</text>
  69. </view>
  70. </scroll-view>
  71. <view class="input-wrapper">
  72. <textarea
  73. class="message-input"
  74. v-model="inputMessage"
  75. placeholder="请输入您的问题..."
  76. maxlength="300"
  77. @confirm="submitQuestion"
  78. confirm-type="send"
  79. @focus="onInputFocus"
  80. :disabled="isProcessing"
  81. auto-height
  82. :maxlength="300"
  83. :style="{ maxHeight: '120rpx' }"
  84. />
  85. <view
  86. class="send-button"
  87. :class="{ 'disabled': !inputMessage.trim() || isProcessing }"
  88. @click="submitQuestion"
  89. hover-class="button-hover"
  90. >
  91. <image
  92. class="send-icon-image"
  93. :src="inputMessage.trim() && !isProcessing ? '/static/icons/chat.png' : '/static/icons/chat_off.png'"
  94. mode="aspectFit"
  95. ></image>
  96. </view>
  97. </view>
  98. </view>
  99. </view>
  100. </template>
  101. <script>
  102. export default {
  103. data() {
  104. return {
  105. inputMessage: '',
  106. chatMessages: [
  107. {
  108. sender: 'ai',
  109. content: '您好!我是农小禹,您的智能农业助手🌱 我可以帮您解答农业种植、病虫害防治、农产品管理等方面的问题。有什么可以帮助您的吗?',
  110. time: this.getFormattedTime(new Date(Date.now() - 3600000)), // 1小时前
  111. timestamp: Date.now() - 3600000,
  112. isWelcome: true
  113. },
  114. {
  115. sender: 'user',
  116. content: '南方现在适合种什么蔬菜?',
  117. time: this.getFormattedTime(new Date(Date.now() - 60000)), // 1分钟前
  118. timestamp: Date.now() - 60000
  119. },
  120. {
  121. sender: 'ai',
  122. content: '您可以考虑种植以下蔬菜:\n\n1. 空心菜:耐热耐湿,生长快速\n2. 丝瓜:适应性强,产量高\n3. 茄子:耐热性好,病虫害较少\n4. 辣椒:喜温喜光,南方气候适宜\n5. 秋葵:抗病性强,营养价值高\n\n记得注意排水和通风,南方雨水多,做好防涝措施。建议早晚浇水,避开中午高温时段。',
  123. time: this.getFormattedTime(new Date()),
  124. timestamp: Date.now()
  125. }
  126. ],
  127. scrollTop: 0,
  128. inputHeight: 120, // 单位px
  129. scrollTimer: null,
  130. isProcessing: false, // 是否正在处理请求
  131. suggestedQuestions: [
  132. '水稻插秧后如何管理?',
  133. '果树夏季修剪技巧?',
  134. '如何防治蔬菜常见病虫害?',
  135. '农药使用注意事项?',
  136. '有机肥和化肥怎么搭配使用?'
  137. ],
  138. statusBarHeight: 20, // 默认值,会在mounted中获取真实值
  139. safeAreaBottom: 34, // 默认值,会在mounted中获取真实值
  140. isIOS: false, // 是否是iOS设备
  141. lastMessageDate: '' // 上一条消息的日期,用于判断是否显示日期分割线
  142. }
  143. },
  144. // 设置页面标题
  145. onNavigationBarButtonTap(e) {
  146. console.log("导航栏按钮点击:", e);
  147. },
  148. mounted() {
  149. // 获取系统信息
  150. const systemInfo = uni.getSystemInfoSync();
  151. this.statusBarHeight = systemInfo.statusBarHeight || 20;
  152. this.isIOS = systemInfo.platform === 'ios';
  153. this.safeAreaBottom = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
  154. // 初始化消息
  155. this.initMessages();
  156. // 滚动到底部
  157. this.$nextTick(() => {
  158. this.scrollToBottom();
  159. });
  160. },
  161. methods: {
  162. onInputFocus() {
  163. // 输入框获取焦点时,确保滚动到底部
  164. this.$nextTick(() => {
  165. this.scrollToBottom();
  166. });
  167. },
  168. scrollToBottom() {
  169. // 使用nextTick确保DOM更新后再滚动
  170. this.$nextTick(() => {
  171. const query = uni.createSelectorQuery().in(this);
  172. query.select('.chat-list').boundingClientRect(data => {
  173. if (data) {
  174. this.scrollTop = data.height + 1000; // 加上足够大的值确保滚动到底部
  175. // 在H5环境下,还可以使用以下方法确保滚动到底部
  176. if (this.isH5) {
  177. setTimeout(() => {
  178. const scrollEl = document.querySelector('.chat-container');
  179. if (scrollEl) {
  180. scrollEl.scrollTop = scrollEl.scrollHeight;
  181. }
  182. }, 100);
  183. }
  184. }
  185. }).exec();
  186. });
  187. },
  188. onScroll(e) {
  189. // 可以添加滚动事件处理
  190. },
  191. getCurrentTime() {
  192. return this.getFormattedTime(new Date());
  193. },
  194. getFormattedTime(date) {
  195. const hours = date.getHours().toString().padStart(2, '0');
  196. const minutes = date.getMinutes().toString().padStart(2, '0');
  197. return `${hours}:${minutes}`;
  198. },
  199. submitQuestion() {
  200. if (!this.inputMessage.trim() || this.isProcessing) return;
  201. this.isProcessing = true;
  202. // 添加用户消息
  203. this.chatMessages.push({
  204. sender: 'user',
  205. content: this.inputMessage,
  206. time: this.getCurrentTime()
  207. });
  208. const userQuestion = this.inputMessage;
  209. this.inputMessage = '';
  210. // 滚动到底部
  211. this.scrollToBottom();
  212. // 先添加一个"正在输入"的消息
  213. this.chatMessages.push({
  214. sender: 'ai',
  215. content: '正在思考...',
  216. time: this.getCurrentTime(),
  217. isTyping: true
  218. });
  219. // 模拟AI回复(实际项目中替换为API调用)
  220. setTimeout(() => {
  221. // 删除"正在输入"消息
  222. this.chatMessages.pop();
  223. // 模拟回复
  224. let aiResponse = "感谢您的提问!作为您的农技助理,我很高兴能帮助您解决农业相关问题。您询问的内容我已收到,我们团队正在研究适合的解决方案。";
  225. if (userQuestion.includes('水稻')) {
  226. aiResponse = "水稻种植需要注意水肥管理和病虫害防治。目前南方地区水稻插秧时间已到,建议您选择抗病性强的品种,如'中优84'。插秧后7天开始浅水促根,分蘖期保持3-5cm水层。";
  227. } else if (userQuestion.includes('蔬菜')) {
  228. aiResponse = "夏季蔬菜种植需注意遮阳和水分管理。建议种植耐热品种如空心菜、苋菜、茄子等。可以采用遮阳网减少强光照射,早晚浇水避开高温时段。";
  229. } else if (userQuestion.includes('果树')) {
  230. aiResponse = "果树现在应注意夏季修剪和病虫害防治。柑橘类果树可以进行夏季修剪,去除徒长枝和内膛枝。同时注意柑橘黄龙病和炭疽病的预防,建议定期喷施药剂保护。";
  231. }
  232. this.chatMessages.push({
  233. sender: 'ai',
  234. content: aiResponse,
  235. time: this.getCurrentTime()
  236. });
  237. this.scrollToBottom();
  238. this.isProcessing = false;
  239. }, 2000);
  240. },
  241. useQuestion(question) {
  242. this.inputMessage = question;
  243. },
  244. containsKeywords(text) {
  245. const keywords = ['水稻', '小麦', '玉米', '病虫害', '农药', '化肥', '有机肥', '种植技术'];
  246. return keywords.some(keyword => text.includes(keyword));
  247. },
  248. formatMessage(text) {
  249. // 将文本中的换行符转换为<br>标签
  250. return text.replace(/\n/g, '<br>');
  251. },
  252. initMessages() {
  253. // 确保消息有时间戳
  254. this.chatMessages.forEach(msg => {
  255. if (!msg.timestamp) {
  256. msg.timestamp = new Date().getTime();
  257. }
  258. });
  259. // 按时间排序
  260. this.chatMessages.sort((a, b) => a.timestamp - b.timestamp);
  261. },
  262. showDateSeparator(index) {
  263. // 判断是否需要显示日期分割线
  264. if (index === 0) return true;
  265. const currentMsg = this.chatMessages[index];
  266. const prevMsg = this.chatMessages[index - 1];
  267. // 如果两条消息相隔超过30分钟,或者是不同日期,显示日期分割线
  268. return this.isDifferentDay(currentMsg.timestamp, prevMsg.timestamp) ||
  269. (currentMsg.timestamp - prevMsg.timestamp > 30 * 60 * 1000);
  270. },
  271. isDifferentDay(timestamp1, timestamp2) {
  272. const date1 = new Date(timestamp1);
  273. const date2 = new Date(timestamp2);
  274. return date1.getDate() !== date2.getDate() ||
  275. date1.getMonth() !== date2.getMonth() ||
  276. date1.getFullYear() !== date2.getFullYear();
  277. },
  278. formatDateSeparator(timestamp) {
  279. const now = new Date();
  280. const msgDate = new Date(timestamp);
  281. // 今天
  282. if (this.isSameDay(msgDate, now)) {
  283. return '今天 ' + this.getFormattedTime(msgDate);
  284. }
  285. // 昨天
  286. const yesterday = new Date(now);
  287. yesterday.setDate(now.getDate() - 1);
  288. if (this.isSameDay(msgDate, yesterday)) {
  289. return '昨天 ' + this.getFormattedTime(msgDate);
  290. }
  291. // 一周内
  292. const oneWeekAgo = new Date(now);
  293. oneWeekAgo.setDate(now.getDate() - 7);
  294. if (msgDate >= oneWeekAgo) {
  295. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  296. return weekdays[msgDate.getDay()] + ' ' + this.getFormattedTime(msgDate);
  297. }
  298. // 其他日期
  299. return msgDate.getFullYear() + '年' + (msgDate.getMonth() + 1) + '月' + msgDate.getDate() + '日 ' + this.getFormattedTime(msgDate);
  300. },
  301. isSameDay(date1, date2) {
  302. return date1.getDate() === date2.getDate() &&
  303. date1.getMonth() === date2.getMonth() &&
  304. date1.getFullYear() === date2.getFullYear();
  305. }
  306. }
  307. }
  308. </script>
  309. <style>
  310. /* 容器样式 */
  311. .container {
  312. position: relative;
  313. min-height: 100vh;
  314. background-color: #f5f5f5;
  315. overflow: hidden; /* 防止内容溢出 */
  316. }
  317. /* 聊天容器 */
  318. .chat-container {
  319. padding: 20rpx 30rpx;
  320. box-sizing: border-box;
  321. background-color: #f8f8f8;
  322. background-image: url('/static/images/chat-bg-pattern.png');
  323. background-size: 300rpx;
  324. background-blend-mode: overlay;
  325. background-opacity: 0.05;
  326. -webkit-overflow-scrolling: touch; /* 增强iOS滚动体验 */
  327. }
  328. .chat-list {
  329. padding-bottom: 30rpx;
  330. }
  331. /* 消息项 */
  332. .message-item {
  333. display: flex;
  334. margin-bottom: 30rpx;
  335. position: relative;
  336. }
  337. .message-ai {
  338. justify-content: flex-start;
  339. }
  340. .message-user {
  341. justify-content: flex-end;
  342. }
  343. /* 头像 */
  344. .avatar-container {
  345. width: 90rpx;
  346. height: 90rpx;
  347. flex-shrink: 0;
  348. }
  349. .avatar {
  350. width: 90rpx;
  351. height: 90rpx;
  352. border-radius: 50%;
  353. background-color: #e0e0e0;
  354. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  355. object-fit: cover;
  356. }
  357. /* 消息内容 */
  358. .message-content {
  359. max-width: 70%;
  360. margin: 0 20rpx;
  361. display: flex;
  362. flex-direction: column;
  363. }
  364. .user-content {
  365. align-items: flex-end;
  366. }
  367. .message-bubble {
  368. padding: 24rpx;
  369. border-radius: 24rpx;
  370. position: relative;
  371. margin-bottom: 10rpx;
  372. word-wrap: break-word;
  373. min-width: 80rpx;
  374. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  375. transition: all 0.3s ease;
  376. max-width: 100%;
  377. }
  378. .ai-bubble {
  379. background-color: #e8f5e9;
  380. border-top-left-radius: 4rpx;
  381. }
  382. .user-bubble {
  383. background-color: #e3f2fd;
  384. border-top-right-radius: 4rpx;
  385. }
  386. .message-text {
  387. font-size: 28rpx;
  388. color: #333;
  389. line-height: 1.5;
  390. word-break: break-all;
  391. }
  392. .message-time {
  393. font-size: 22rpx;
  394. color: #999;
  395. }
  396. /* 输入区域 */
  397. .input-container {
  398. position: fixed;
  399. bottom: 0;
  400. left: 0;
  401. right: 0;
  402. background-color: #fff;
  403. padding: 20rpx 30rpx;
  404. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
  405. display: flex;
  406. flex-direction: column;
  407. z-index: 10;
  408. }
  409. .input-wrapper {
  410. display: flex;
  411. align-items: flex-end;
  412. }
  413. .message-input {
  414. flex: 1;
  415. min-height: 70rpx;
  416. max-height: 120rpx;
  417. border-radius: 35rpx;
  418. background-color: #f5f5f5;
  419. padding: 15rpx 30rpx;
  420. font-size: 28rpx;
  421. color: #333;
  422. border: 1rpx solid #e0e0e0;
  423. line-height: 1.4;
  424. }
  425. .send-button {
  426. margin-left: 16rpx;
  427. width: 76rpx;
  428. height: 76rpx;
  429. border-radius: 50%;
  430. background-color: transparent;
  431. background-image: none;
  432. display: flex;
  433. align-items: center;
  434. justify-content: center;
  435. transition: all 0.2s ease;
  436. position: relative;
  437. align-self: center;
  438. }
  439. .send-button.disabled {
  440. background-color: transparent;
  441. background-image: none;
  442. opacity: 1;
  443. }
  444. .button-hover {
  445. transform: scale(0.95);
  446. }
  447. @keyframes pulse {
  448. 0% { transform: scale(1); }
  449. 50% { transform: scale(0.95); }
  450. 100% { transform: scale(1); }
  451. }
  452. .send-button:active:not(.disabled) {
  453. animation: pulse 0.3s ease-in-out;
  454. }
  455. /* 删除或注释掉之前的样式 */
  456. .send-icon {
  457. display: none;
  458. }
  459. .send-icon:before {
  460. display: none;
  461. }
  462. .send-icon-text {
  463. display: none;
  464. }
  465. /* 推荐问题区域 */
  466. .suggested-questions {
  467. display: flex;
  468. white-space: nowrap;
  469. margin-bottom: 15rpx;
  470. padding: 5rpx 0;
  471. }
  472. .question-chip {
  473. display: inline-block;
  474. padding: 12rpx 20rpx;
  475. margin-right: 15rpx;
  476. background-color: #e8f5e9;
  477. color: #4CAF50;
  478. font-size: 24rpx;
  479. border-radius: 30rpx;
  480. border: 1rpx solid #a5d6a7;
  481. }
  482. /* AI正在输入的样式 */
  483. .message-bubble.ai-bubble.typing {
  484. background-color: #f0f0f0;
  485. }
  486. .typing-indicator {
  487. display: flex;
  488. align-items: center;
  489. justify-content: center;
  490. height: 40rpx;
  491. padding: 0 20rpx;
  492. }
  493. .typing-dot {
  494. width: 10rpx;
  495. height: 10rpx;
  496. margin: 0 5rpx;
  497. background-color: #4CAF50;
  498. border-radius: 50%;
  499. opacity: 0.5;
  500. animation: typingAnimation 1.4s infinite both;
  501. }
  502. .typing-dot:nth-child(2) {
  503. animation-delay: 0.2s;
  504. }
  505. .typing-dot:nth-child(3) {
  506. animation-delay: 0.4s;
  507. }
  508. @keyframes typingAnimation {
  509. 0% { opacity: 0.3; transform: translateY(0); }
  510. 50% { opacity: 1; transform: translateY(-5rpx); }
  511. 100% { opacity: 0.3; transform: translateY(0); }
  512. }
  513. /* 关键词高亮 */
  514. .message-text.highlight {
  515. color: #2E7D32;
  516. font-weight: 500;
  517. }
  518. /* 欢迎消息特殊样式 */
  519. .welcome {
  520. background-color: #e3f2fd !important;
  521. border-left: none !important;
  522. border-radius: 24rpx !important;
  523. }
  524. /* 日期分割线样式 */
  525. .date-separator {
  526. display: flex;
  527. align-items: center;
  528. justify-content: center;
  529. margin: 20rpx 0;
  530. }
  531. .date-separator text {
  532. background-color: rgba(0, 0, 0, 0.1);
  533. color: #666;
  534. font-size: 24rpx;
  535. padding: 4rpx 20rpx;
  536. border-radius: 20rpx;
  537. }
  538. /* 纸飞机图标 */
  539. .plane-svg {
  540. display: none;
  541. }
  542. .send-icon-image {
  543. width: 76rpx;
  544. height: 76rpx;
  545. }
  546. .material-icon {
  547. font-family: 'Material Icons';
  548. font-weight: normal;
  549. font-style: normal;
  550. font-size: 48rpx;
  551. line-height: 1;
  552. letter-spacing: normal;
  553. text-transform: none;
  554. display: inline-block;
  555. white-space: nowrap;
  556. word-wrap: normal;
  557. direction: ltr;
  558. -webkit-font-smoothing: antialiased;
  559. color: white;
  560. }
  561. /* 删除不需要的导航栏样式 */
  562. .custom-navbar {
  563. display: none;
  564. }
  565. .navbar-bg, .navbar-content, .navbar-left, .navbar-title, .navbar-right, .back-icon, .arrow-left {
  566. display: none;
  567. }
  568. </style>