index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. <template>
  2. <view class="container">
  3. <!-- 聊天记录区域 -->
  4. <scroll-view class="chat-container" scroll-y :scroll-top="scrollTop" :scroll-with-animation="true"
  5. @scroll="onScroll" :style="{
  6. height: `calc(100vh - ${inputHeight}px)`,
  7. marginTop: '0'
  8. }">
  9. <view class="chat-list">
  10. <view v-for="(message, index) in chatMessages" :key="index" class="message-item"
  11. :class="{ 'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user' }">
  12. <!-- AI消息 -->
  13. <template v-if="message.sender === 'ai'">
  14. <view class="avatar-container">
  15. <image class="avatar" src="/static/icons/ai.png" mode="aspectFill"></image>
  16. </view>
  17. <view class="message-content">
  18. <view class="message-bubble ai-bubble"
  19. :class="{'typing': message.isTyping, 'welcome': message.isWelcome || index === 0}">
  20. <view v-if="message.isTyping" class="typing-indicator">
  21. <view class="typing-dot"></view>
  22. <view class="typing-dot"></view>
  23. <view class="typing-dot"></view>
  24. </view>
  25. <text v-else class="message-text"
  26. :class="{'highlight': containsKeywords(message.content)}">
  27. {{ message.content }}
  28. </text>
  29. </view>
  30. <text class="message-time">{{ message.time }}</text>
  31. </view>
  32. </template>
  33. <!-- 用户消息 -->
  34. <template v-else>
  35. <view class="message-content user-content">
  36. <text class="message-time">{{ message.time }}</text>
  37. <view class="message-bubble user-bubble">
  38. <text class="message-text">{{ message.content }}</text>
  39. </view>
  40. </view>
  41. <view class="avatar-container">
  42. <image class="avatar" src="/static/images/user-avatar.svg" mode="aspectFill"></image>
  43. </view>
  44. </template>
  45. </view>
  46. </view>
  47. </scroll-view>
  48. <!-- 底部输入区 -->
  49. <view class="input-container" :style="{ paddingBottom: `${isIOS ? safeAreaBottom : 20}rpx` }">
  50. <!-- 问题建议区 -->
  51. <scroll-view v-if="chatMessages.length <= 3 && !inputMessage" class="suggested-questions" scroll-x>
  52. <view v-for="(question, index) in suggestedQuestions" :key="index" class="question-chip"
  53. @click="useQuestion(question)">
  54. <text>{{ question }}</text>
  55. </view>
  56. </scroll-view>
  57. <view class="input-wrapper">
  58. <textarea class="message-input" v-model="inputMessage" placeholder="请输入您的问题..." :disabled="isProcessing"
  59. auto-height :maxlength="300" :style="{ maxHeight: '120rpx' }" @focus="onInputFocus"
  60. @confirm="submitQuestion" />
  61. <view class="send-button" :class="{ 'disabled': !inputMessage.trim() || isProcessing }"
  62. @click="submitQuestion" hover-class="button-hover">
  63. <image class="send-icon-image"
  64. :src="inputMessage.trim() && !isProcessing ? '/static/icons/chat.png' : '/static/icons/chat_off.png'"
  65. mode="aspectFit"></image>
  66. </view>
  67. </view>
  68. </view>
  69. </view>
  70. </template>
  71. <script>
  72. import api from "@/config/api.js";
  73. import storage from "@/utils/storage.js";
  74. export default {
  75. data() {
  76. return {
  77. inputMessage: '', // 输入框消息
  78. chatMessages: [{
  79. sender: 'ai',
  80. content: '您好!我是农小禹,您的智能农业助手🌱 我可以帮您解答农业种植、病虫害防治、农产品管理等方面的问题。有什么可以帮助您的吗?',
  81. time: this.getFormattedTime(new Date()),
  82. timestamp: Date.now(),
  83. isWelcome: true
  84. }],
  85. scrollTop: 0,
  86. inputHeight: 110,
  87. isProcessing: false,
  88. requestTask: null,
  89. currentTypingMessage: null, // 当前正在输入的消息索引
  90. suggestedQuestions: [
  91. '水稻插秧后如何管理?',
  92. '果树夏季修剪技巧?',
  93. '如何防治蔬菜常见病虫害?',
  94. '农药使用注意事项?',
  95. '有机肥和化肥怎么搭配使用?'
  96. ],
  97. statusBarHeight: 20,
  98. safeAreaBottom: 34,
  99. isIOS: false,
  100. typingInterval: 100, // 打字速度配置
  101. typingTimers: [] // 用于存储定时器
  102. }
  103. },
  104. // 设置页面标题
  105. onNavigationBarButtonTap(e) {
  106. console.log("导航栏按钮点击:", e);
  107. },
  108. created() {
  109. this.debouncedSubmitQuestion = this.debounce(this.submitQuestion, 300)
  110. },
  111. mounted() {
  112. // 获取系统信息
  113. const systemInfo = uni.getSystemInfoSync();
  114. this.statusBarHeight = systemInfo.statusBarHeight || 20;
  115. this.isIOS = systemInfo.platform === 'ios';
  116. this.safeAreaBottom = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
  117. // 初始化消息
  118. this.initMessages();
  119. // 滚动到底部
  120. this.$nextTick(() => {
  121. this.scrollToBottom();
  122. });
  123. },
  124. methods: {
  125. // 处理消息发送的方法
  126. makeStreamRequest(message) {
  127. if (this.requestTask) {
  128. this.requestTask.abort();
  129. }
  130. const url = api.serve + '/uniapp/dify/chat/stream';
  131. // 添加一个临时的"正在输入"消息
  132. const typingMessageIndex = this.chatMessages.push({
  133. sender: 'ai',
  134. content: '',
  135. time: this.getCurrentTime(),
  136. isTyping: true
  137. }) - 1;
  138. this.currentTypingMessage = typingMessageIndex;
  139. this.requestTask = uni.request({
  140. url: url,
  141. method: 'POST',
  142. data: message,
  143. responseType: 'text',
  144. header: {
  145. 'content-type': 'application/json',
  146. 'Authorization': `Bearer ${storage.getAccessToken()}`
  147. },
  148. success: (res) => {
  149. if (res.data) {
  150. const lines = res.data.split('\n');
  151. this.processSSELines(lines, typingMessageIndex);
  152. }
  153. },
  154. fail: (err) => {
  155. console.error('请求失败:', err);
  156. this.handleError(err);
  157. // 移除"正在输入"消息
  158. if (this.currentTypingMessage !== null) {
  159. this.chatMessages.splice(this.currentTypingMessage, 1);
  160. }
  161. },
  162. complete: () => {
  163. this.requestTask = null;
  164. this.isProcessing = false;
  165. this.scrollToBottom();
  166. }
  167. });
  168. },
  169. // 处理 SSE 数据行
  170. processSSELines(lines, typingMessageIndex) {
  171. let accumulatedText = this.chatMessages[typingMessageIndex]?.content || '';
  172. lines.forEach(line => {
  173. if (!line.trim()) return;
  174. if (line.startsWith('data:')) {
  175. try {
  176. const jsonData = JSON.parse(line.slice(5).trim());
  177. if (jsonData.eventType === 'AGENT_MESSAGE' && jsonData.answer) {
  178. accumulatedText += jsonData.answer;
  179. // 使用新的打字机效果更新消息内容
  180. this.typewriterEffect(accumulatedText, (text) => {
  181. this.$set(this.chatMessages[typingMessageIndex], 'content', text);
  182. // 滚动到底部,确保用户看到最新内容
  183. this.scrollToBottom();
  184. });
  185. } else if (jsonData.eventType === 'MESSAGE_END') {
  186. // 消息结束时停止打字效果
  187. this.currentTypingMessage = null;
  188. this.$set(this.chatMessages[typingMessageIndex], 'isTyping', false);
  189. // 确保完整内容显示
  190. }
  191. } catch (e) {
  192. console.error('解析数据出错:', e);
  193. }
  194. }
  195. });
  196. },
  197. // 实现打字机效果的方法
  198. typewriterEffect(text, callback) {
  199. let index = 0;
  200. const length = text.length;
  201. const interval = 50; // 打字间隔时间
  202. // 清除之前的打字机效果定时器
  203. this.typingTimers.forEach(timer => clearTimeout(timer));
  204. this.typingTimers = [];
  205. const type = () => {
  206. if (index <= length) {
  207. callback(text.slice(0, index));
  208. index++;
  209. const timer = setTimeout(type, interval);
  210. this.typingTimers.push(timer);
  211. }
  212. };
  213. type();
  214. },
  215. // 发送消息
  216. submitQuestion() {
  217. if (!storage.getHasLogin()) {
  218. // 使用 uni.showModal 显示登录提示框
  219. uni.showModal({
  220. title: '提示',
  221. content: '您还未登录,请先登录',
  222. confirmText: '去登录',
  223. cancelText: '取消',
  224. success: function(res) {
  225. if (res.confirm) {
  226. // 点击确定按钮,跳转到登录页面
  227. uni.navigateTo({
  228. url: '/pages/login/index'
  229. });
  230. }
  231. },
  232. });
  233. return;
  234. }
  235. if (!this.inputMessage.trim() || this.isProcessing) return;
  236. this.isProcessing = true;
  237. // 添加用户消息
  238. this.chatMessages.push({
  239. sender: 'user',
  240. content: this.inputMessage,
  241. time: this.getCurrentTime()
  242. });
  243. const message = {
  244. query: this.inputMessage,
  245. user: 'user_' + Date.now()
  246. };
  247. this.inputMessage = '';
  248. this.scrollToBottom();
  249. this.makeStreamRequest(message);
  250. },
  251. // 添加错误处理方法
  252. handleError(error) {
  253. let errorMessage = '发生错误'
  254. if (error.errMsg) {
  255. errorMessage = error.errMsg
  256. } else if (error.message) {
  257. errorMessage = error.message
  258. }
  259. uni.showToast({
  260. title: errorMessage,
  261. icon: 'none',
  262. duration: 2000
  263. })
  264. },
  265. // 添加防抖函数
  266. debounce(func, wait) {
  267. let timeout
  268. return (...args) => {
  269. clearTimeout(timeout)
  270. timeout = setTimeout(() => {
  271. func.apply(this, args)
  272. }, wait)
  273. }
  274. },
  275. onInputFocus() {
  276. // 输入框获取焦点时,确保滚动到底部
  277. this.$nextTick(() => {
  278. this.scrollToBottom();
  279. });
  280. },
  281. // 滚动到底部
  282. scrollToBottom() {
  283. this.$nextTick(() => {
  284. const query = uni.createSelectorQuery().in(this)
  285. query.select('.chat-list').boundingClientRect(data => {
  286. if (data) {
  287. this.scrollTop = data.height + 1000
  288. }
  289. }).exec()
  290. })
  291. },
  292. // 错误处理
  293. handleError(error) {
  294. let errorMessage = '发生错误'
  295. if (error.errMsg) {
  296. errorMessage = error.errMsg
  297. } else if (error.message) {
  298. errorMessage = error.message
  299. }
  300. uni.showToast({
  301. title: errorMessage,
  302. icon: 'none',
  303. duration: 2000
  304. })
  305. },
  306. onScroll(e) {
  307. // 可以添加滚动事件处理
  308. },
  309. getCurrentTime() {
  310. return this.getFormattedTime(new Date());
  311. },
  312. getFormattedTime(date) {
  313. const hours = date.getHours().toString().padStart(2, '0');
  314. const minutes = date.getMinutes().toString().padStart(2, '0');
  315. return `${hours}:${minutes}`;
  316. },
  317. /* submitQuestion() {
  318. if (!this.inputMessage.trim() || this.isProcessing) return;
  319. this.isProcessing = true;
  320. // 添加用户消息
  321. this.chatMessages.push({
  322. sender: 'user',
  323. content: this.inputMessage,
  324. time: this.getCurrentTime()
  325. });
  326. const userQuestion = this.inputMessage;
  327. this.inputMessage = '';
  328. // 滚动到底部
  329. this.scrollToBottom();
  330. // 先添加一个"正在输入"的消息
  331. this.chatMessages.push({
  332. sender: 'ai',
  333. content: '正在思考...',
  334. time: this.getCurrentTime(),
  335. isTyping: true
  336. });
  337. // 模拟AI回复(实际项目中替换为API调用)
  338. setTimeout(() => {
  339. // 删除"正在输入"消息
  340. this.chatMessages.pop();
  341. // 模拟回复
  342. let aiResponse = "感谢您的提问!作为您的农技助理,我很高兴能帮助您解决农业相关问题。您询问的内容我已收到,我们团队正在研究适合的解决方案。";
  343. if (userQuestion.includes('水稻')) {
  344. aiResponse = "水稻种植需要注意水肥管理和病虫害防治。目前南方地区水稻插秧时间已到,建议您选择抗病性强的品种,如'中优84'。插秧后7天开始浅水促根,分蘖期保持3-5cm水层。";
  345. } else if (userQuestion.includes('蔬菜')) {
  346. aiResponse = "夏季蔬菜种植需注意遮阳和水分管理。建议种植耐热品种如空心菜、苋菜、茄子等。可以采用遮阳网减少强光照射,早晚浇水避开高温时段。";
  347. } else if (userQuestion.includes('果树')) {
  348. aiResponse = "果树现在应注意夏季修剪和病虫害防治。柑橘类果树可以进行夏季修剪,去除徒长枝和内膛枝。同时注意柑橘黄龙病和炭疽病的预防,建议定期喷施药剂保护。";
  349. }
  350. this.chatMessages.push({
  351. sender: 'ai',
  352. content: aiResponse,
  353. time: this.getCurrentTime()
  354. });
  355. this.scrollToBottom();
  356. this.isProcessing = false;
  357. }, 2000);
  358. }, */
  359. useQuestion(question) {
  360. this.inputMessage = question;
  361. },
  362. containsKeywords(text) {
  363. const keywords = ['水稻', '小麦', '玉米', '病虫害', '农药', '化肥', '有机肥', '种植技术'];
  364. return keywords.some(keyword => text.includes(keyword));
  365. },
  366. formatMessage(text) {
  367. // 将文本中的换行符转换为<br>标签
  368. return text.replace(/\n/g, '<br>');
  369. },
  370. initMessages() {
  371. // 确保消息有时间戳
  372. this.chatMessages.forEach(msg => {
  373. if (!msg.timestamp) {
  374. msg.timestamp = new Date().getTime();
  375. }
  376. });
  377. // 按时间排序
  378. this.chatMessages.sort((a, b) => a.timestamp - b.timestamp);
  379. },
  380. showDateSeparator(index) {
  381. // 判断是否需要显示日期分割线
  382. if (index === 0) return true;
  383. const currentMsg = this.chatMessages[index];
  384. const prevMsg = this.chatMessages[index - 1];
  385. // 如果两条消息相隔超过30分钟,或者是不同日期,显示日期分割线
  386. return this.isDifferentDay(currentMsg.timestamp, prevMsg.timestamp) ||
  387. (currentMsg.timestamp - prevMsg.timestamp > 30 * 60 * 1000);
  388. },
  389. isDifferentDay(timestamp1, timestamp2) {
  390. const date1 = new Date(timestamp1);
  391. const date2 = new Date(timestamp2);
  392. return date1.getDate() !== date2.getDate() ||
  393. date1.getMonth() !== date2.getMonth() ||
  394. date1.getFullYear() !== date2.getFullYear();
  395. },
  396. formatDateSeparator(timestamp) {
  397. const now = new Date();
  398. const msgDate = new Date(timestamp);
  399. // 今天
  400. if (this.isSameDay(msgDate, now)) {
  401. return '今天 ' + this.getFormattedTime(msgDate);
  402. }
  403. // 昨天
  404. const yesterday = new Date(now);
  405. yesterday.setDate(now.getDate() - 1);
  406. if (this.isSameDay(msgDate, yesterday)) {
  407. return '昨天 ' + this.getFormattedTime(msgDate);
  408. }
  409. // 一周内
  410. const oneWeekAgo = new Date(now);
  411. oneWeekAgo.setDate(now.getDate() - 7);
  412. if (msgDate >= oneWeekAgo) {
  413. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  414. return weekdays[msgDate.getDay()] + ' ' + this.getFormattedTime(msgDate);
  415. }
  416. // 其他日期
  417. return msgDate.getFullYear() + '年' + (msgDate.getMonth() + 1) + '月' + msgDate.getDate() + '日 ' +
  418. this
  419. .getFormattedTime(msgDate);
  420. },
  421. isSameDay(date1, date2) {
  422. return date1.getDate() === date2.getDate() &&
  423. date1.getMonth() === date2.getMonth() &&
  424. date1.getFullYear() === date2.getFullYear();
  425. }
  426. },
  427. // 组件销毁时清理资源
  428. beforeDestroy() {
  429. // 清理所有打字机效果的定时器
  430. this.typingTimers.forEach(timer => clearTimeout(timer));
  431. // 清理请求
  432. if (this.requestTask) {
  433. this.requestTask.abort();
  434. this.requestTask = null;
  435. }
  436. }
  437. }
  438. </script>
  439. <style>
  440. /* 容器样式 */
  441. .container {
  442. position: relative;
  443. min-height: 100vh;
  444. background-color: #f5f5f5;
  445. overflow: hidden;
  446. /* 防止内容溢出 */
  447. }
  448. /* 聊天容器 */
  449. .chat-container {
  450. padding: 20rpx 30rpx;
  451. box-sizing: border-box;
  452. background-color: #f8f8f8;
  453. background-image: url('/static/images/chat-bg-pattern.png');
  454. background-size: 300rpx;
  455. background-blend-mode: overlay;
  456. background-opacity: 0.05;
  457. -webkit-overflow-scrolling: touch;
  458. /* 增强iOS滚动体验 */
  459. }
  460. .chat-list {
  461. padding-bottom: 30rpx;
  462. }
  463. /* 消息项 */
  464. .message-item {
  465. display: flex;
  466. margin-bottom: 30rpx;
  467. position: relative;
  468. }
  469. .message-ai {
  470. justify-content: flex-start;
  471. }
  472. .message-user {
  473. justify-content: flex-end;
  474. }
  475. /* 头像 */
  476. .avatar-container {
  477. width: 90rpx;
  478. height: 90rpx;
  479. flex-shrink: 0;
  480. }
  481. .avatar {
  482. width: 90rpx;
  483. height: 90rpx;
  484. border-radius: 50%;
  485. background-color: #e0e0e0;
  486. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  487. object-fit: cover;
  488. }
  489. /* 消息内容 */
  490. .message-content {
  491. max-width: 70%;
  492. margin: 0 20rpx;
  493. display: flex;
  494. flex-direction: column;
  495. }
  496. .user-content {
  497. align-items: flex-end;
  498. }
  499. .message-bubble {
  500. padding: 24rpx;
  501. border-radius: 24rpx;
  502. position: relative;
  503. margin-bottom: 10rpx;
  504. word-wrap: break-word;
  505. min-width: 80rpx;
  506. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  507. transition: all 0.3s ease;
  508. max-width: 100%;
  509. }
  510. .ai-bubble {
  511. background-color: #e8f5e9;
  512. border-top-left-radius: 4rpx;
  513. }
  514. .user-bubble {
  515. background-color: #e3f2fd;
  516. border-top-right-radius: 4rpx;
  517. }
  518. .message-text {
  519. font-size: 28rpx;
  520. color: #333;
  521. line-height: 1.5;
  522. word-break: break-all;
  523. }
  524. .message-time {
  525. font-size: 22rpx;
  526. color: #999;
  527. }
  528. /* 输入区域 */
  529. .input-container {
  530. position: fixed;
  531. bottom: 0;
  532. left: 0;
  533. right: 0;
  534. background-color: #fff;
  535. padding: 20rpx 30rpx;
  536. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
  537. display: flex;
  538. flex-direction: column;
  539. z-index: 10;
  540. }
  541. .input-wrapper {
  542. display: flex;
  543. align-items: flex-end;
  544. }
  545. .message-input {
  546. flex: 1;
  547. min-height: 70rpx;
  548. max-height: 120rpx;
  549. border-radius: 35rpx;
  550. background-color: #f5f5f5;
  551. padding: 15rpx 30rpx;
  552. font-size: 28rpx;
  553. color: #333;
  554. border: 1rpx solid #e0e0e0;
  555. line-height: 1.4;
  556. }
  557. .send-button {
  558. margin-left: 16rpx;
  559. width: 76rpx;
  560. height: 76rpx;
  561. border-radius: 50%;
  562. background-color: transparent;
  563. background-image: none;
  564. display: flex;
  565. align-items: center;
  566. justify-content: center;
  567. transition: all 0.2s ease;
  568. position: relative;
  569. align-self: center;
  570. }
  571. .send-button.disabled {
  572. background-color: transparent;
  573. background-image: none;
  574. opacity: 1;
  575. }
  576. .button-hover {
  577. transform: scale(0.95);
  578. }
  579. @keyframes pulse {
  580. 0% {
  581. transform: scale(1);
  582. }
  583. 50% {
  584. transform: scale(0.95);
  585. }
  586. 100% {
  587. transform: scale(1);
  588. }
  589. }
  590. .send-button:active:not(.disabled) {
  591. animation: pulse 0.3s ease-in-out;
  592. }
  593. /* 删除或注释掉之前的样式 */
  594. .send-icon {
  595. display: none;
  596. }
  597. .send-icon:before {
  598. display: none;
  599. }
  600. .send-icon-text {
  601. display: none;
  602. }
  603. /* 推荐问题区域 */
  604. .suggested-questions {
  605. display: flex;
  606. white-space: nowrap;
  607. margin-bottom: 15rpx;
  608. padding: 5rpx 0;
  609. }
  610. .question-chip {
  611. display: inline-block;
  612. padding: 12rpx 20rpx;
  613. margin-right: 15rpx;
  614. background-color: #e8f5e9;
  615. color: #4CAF50;
  616. font-size: 24rpx;
  617. border-radius: 30rpx;
  618. border: 1rpx solid #a5d6a7;
  619. }
  620. /* AI正在输入的样式 */
  621. .message-bubble.ai-bubble.typing {
  622. background-color: #f0f0f0;
  623. }
  624. .typing-indicator {
  625. display: flex;
  626. align-items: center;
  627. justify-content: center;
  628. height: 40rpx;
  629. padding: 0 20rpx;
  630. }
  631. .typing-dot {
  632. width: 10rpx;
  633. height: 10rpx;
  634. margin: 0 5rpx;
  635. background-color: #4CAF50;
  636. border-radius: 50%;
  637. opacity: 0.5;
  638. animation: typingAnimation 1.4s infinite both;
  639. }
  640. .typing-dot:nth-child(2) {
  641. animation-delay: 0.2s;
  642. }
  643. .typing-dot:nth-child(3) {
  644. animation-delay: 0.4s;
  645. }
  646. @keyframes typingAnimation {
  647. 0% {
  648. opacity: 0.3;
  649. transform: translateY(0);
  650. }
  651. 50% {
  652. opacity: 1;
  653. transform: translateY(-5rpx);
  654. }
  655. 100% {
  656. opacity: 0.3;
  657. transform: translateY(0);
  658. }
  659. }
  660. /* 关键词高亮 */
  661. .message-text.highlight {
  662. color: #2E7D32;
  663. font-weight: 500;
  664. }
  665. /* 欢迎消息特殊样式 */
  666. .welcome {
  667. background-color: #e3f2fd !important;
  668. border-left: none !important;
  669. border-radius: 24rpx !important;
  670. }
  671. /* 日期分割线样式 */
  672. .date-separator {
  673. display: flex;
  674. align-items: center;
  675. justify-content: center;
  676. margin: 20rpx 0;
  677. }
  678. .date-separator text {
  679. background-color: rgba(0, 0, 0, 0.1);
  680. color: #666;
  681. font-size: 24rpx;
  682. padding: 4rpx 20rpx;
  683. border-radius: 20rpx;
  684. }
  685. /* 纸飞机图标 */
  686. .plane-svg {
  687. display: none;
  688. }
  689. .send-icon-image {
  690. width: 76rpx;
  691. height: 76rpx;
  692. }
  693. .material-icon {
  694. font-family: 'Material Icons';
  695. font-weight: normal;
  696. font-style: normal;
  697. font-size: 48rpx;
  698. line-height: 1;
  699. letter-spacing: normal;
  700. text-transform: none;
  701. display: inline-block;
  702. white-space: nowrap;
  703. word-wrap: normal;
  704. direction: ltr;
  705. -webkit-font-smoothing: antialiased;
  706. color: white;
  707. }
  708. /* 删除不需要的导航栏样式 */
  709. .custom-navbar {
  710. display: none;
  711. }
  712. .navbar-bg,
  713. .navbar-content,
  714. .navbar-left,
  715. .navbar-title,
  716. .navbar-right,
  717. .back-icon,
  718. .arrow-left {
  719. display: none;
  720. }
  721. </style>