乐于分享
好东西不私藏

uniapp AI聊天页面

uniapp AI聊天页面

已关注

关注

重播 分享

一键复制即可测试效果
页面采用经典的聊天应用布局,从上到下依次为:状态栏适配的导航区域、消息展示区域、快捷提示词区域、多功能输入区域。整体使用Flex布局实现垂直方向的自适应,确保在不同屏幕尺寸下都能良好展示。
AI回复采用逐字打印的流式效果,通过typingMessages对象管理每个消息的打字状态,使用递归setTimeout实现逐字输出,同时配合$forceUpdate强制刷新视图,营造AI正在思考并逐字回复的沉浸体验。
集成DeepSeek AI接口,通过uni.request发起HTTPS请求,携带用户消息获取AI回复。
  • 顶部导航栏适配小程序胶囊按钮高度

  • 消息时间戳格式化显示

  • 提示词区域横向滚动,提供快捷输入

  • 发送按钮根据输入内容动态高亮

  • 录音时全局遮罩,防止误触

源码

<template><viewclass="chat-container"><!-- 顶部导航栏 - 风格,与胶囊按钮对齐 --><viewclass="chat-header":style="{ paddingTop: statusBarHeight + 'px', height: headerHeight + 'px' }"><viewclass="header-left" @click="goBack"><imagesrc="/static/back.png"class="func-img"mode="aspectFit"></image></view><viewclass="header-center"><textclass="bot-name"></text><viewclass="status-badge"><textclass="status-dot"></text><textclass="status-text">智能助手</text></view></view><viewclass="header-right"><textclass="more-icon"></text></view></view><!-- 聊天内容区域 --><scroll-viewscroll-yclass="chat-content":scroll-top="scrollTop" @scrolltoupper="loadMoreMessages":scroll-with-animation="true":enable-back-to-top="true":scroll-y="true":throttle="false"><viewclass="chat-messages"><!-- 时间分隔 --><viewclass="time-divider"v-if="showDateDivider"><text>{{ today }}</text></view><!-- 消息列表 --><viewv-for="(message, index) in messages":key="message.id"><!-- AI消息 --><viewv-if="message.sender !== 'me'"class="message-item other-message"><viewclass="message-content ai-message-content"><viewclass="message-bubble other-bubble ai-bubble"><!-- 图片消息 --><imagev-if="message.imageUrl":src="message.imageUrl"class="message-image"mode="widthFix" @click="previewImage(message.imageUrl)"></image><!-- 文本消息 --><textv-elseclass="message-text ai-message-text">{{ displayContent(message) }}</text><viewclass="typing-indicator"v-if="message.typing"><viewclass="dot"></view><viewclass="dot"></view><viewclass="dot"></view></view></view><viewclass="message-time">                {{ formatTime(message.time) }}</view></view></view><!-- 用户消息 --><viewv-elseclass="message-item my-message"><viewclass="message-content my-content"><!-- 语音消息 --><viewv-if="message.voiceUrl"class="message-bubble my-bubble voice-bubble"                @click="playVoice(message)"><imagesrc="/static/voice-icon.png"class="voice-img"mode="aspectFit"></image><textclass="voice-duration">{{ message.duration || 0 }}"</text></view><!-- 图片消息 --><viewv-else-if="message.imageUrl"class="message-bubble my-bubble image-bubble"                @click="previewImage(message.imageUrl)"><image:src="message.imageUrl"class="message-image"mode="widthFix"></image></view><!-- 文本消息 --><viewv-elseclass="message-bubble my-bubble"><textclass="message-text">{{ message.content }}</text></view><viewclass="message-time my-time">                {{ formatTime(message.time) }}</view></view></view></view></view></scroll-view><!-- 提示词区域 - 横向滚动,固定在输入框上方 --><viewclass="tip-wrapper"><scroll-viewscroll-xclass="tip-scroll":show-scrollbar="false"><viewclass="tip-list"><viewclass="tip-item" @click="sendTip('帮我写一份工作周报')">帮我写一份工作周报</view><viewclass="tip-item" @click="sendTip('周末去哪里玩比较好')">周末去哪里玩比较好</view><viewclass="tip-item" @click="sendTip('推荐一部好看的电影')">推荐一部好看的电影</view><viewclass="tip-item" @click="sendTip('怎么做红烧肉')">怎么做红烧肉</view><viewclass="tip-item" @click="sendTip('给我讲个笑话')">给我讲个笑话</view><viewclass="tip-item" @click="sendTip('如何提高工作效率')">如何提高工作效率</view><viewclass="tip-item" @click="sendTip('今天天气怎么样')">今天天气怎么样</view><viewclass="tip-item" @click="sendTip('学习英语有什么技巧')">学习英语有什么技巧</view><viewclass="tip-item" @click="sendTip('推荐几本好书')">推荐几本好书</view><viewclass="tip-item" @click="sendTip('怎么缓解压力')">怎么缓解压力</view></view></scroll-view></view><!-- 输入区域 - 风格 --><viewclass="input-area"><!-- 录音状态显示 --><viewclass="recording-mask"v-if="isRecording" @touchend="stopRecord" @touchcancel="cancelRecord"><viewclass="recording-content"><viewclass="recording-animation"><viewclass="recording-wave"></view><viewclass="recording-wave"></view><viewclass="recording-wave"></view></view><textclass="recording-text">正在录音 {{ recordingTime }}s</text><textclass="recording-tip">松开发送,上滑取消</text></view></view><viewclass="input-container"><!-- 功能按钮区域 - 纯图片 --><viewclass="function-buttons"><viewclass="func-btn" @click="toggleMorePanel"><imagesrc="/static/add-icon.png"class="func-img"mode="aspectFit"></image></view></view><!-- 输入框区域 --><viewclass="input-wrapper"><inputtype="text"v-model="inputMessage"placeholder="发消息或按住说话..."class="message-input"            @confirm="sendMessage" @focus="hideAllPanels"placeholder-class="placeholder-style" /><viewclass="voice-input-btn" @touchstart="startRecord" @touchend="stopRecord"            @touchcancel="cancelRecord"><imagesrc="/static/voice-input-icon.png"class="voice-input-img"mode="aspectFit"></image></view></view><!-- 发送按钮 --><buttonclass="send-btn":class="{ active: inputMessage.trim() }" @click="sendMessage":disabled="isSending">          {{ isSending ? '...' : '发送' }}</button></view><!-- 更多功能面板 - 纯图片图标 --><viewclass="more-panel"v-if="showMorePanel"><viewclass="more-grid"><viewclass="more-item" @click="uploadImage"><viewclass="more-icon-bg"><imagesrc="/static/photo-icon.png"class="more-img"mode="aspectFit"></image></view><textclass="more-text">相册</text></view><viewclass="more-item" @click="takePhoto"><viewclass="more-icon-bg"><imagesrc="/static/camera-icon.png"class="more-img"mode="aspectFit"></image></view><textclass="more-text">拍照</text></view></view></view></view></view></template><script>exportdefault {    data() {return {inputMessage'',showMorePanelfalse,scrollTop0,loadingfalse,isSendingfalse,isRecordingfalse,recordingTime0,recordingTimernull,tempFilePath'',typingMessages: {},typingSpeed30// 加快打字速度        statusBarHeight: 0,headerHeight0,messages: [{id'msg_001',sender'other',content'你好,我是AI智能助手!有什么可以帮助你的吗?😊',timenewDate().getTime() - 3600000 * 2        }]      }    },computed: {      today() {const date = newDate()return`${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`      },      showDateDivider() {returnthis.messages.some(msg => {const msgDate = newDate(msg.time)const today = newDate()return (            msgDate.getDate() === today.getDate() &&            msgDate.getMonth() === today.getMonth() &&            msgDate.getFullYear() === today.getFullYear()          )        })      }    },methods: {      getSystemInfo() {const systemInfo = uni.getSystemInfoSync()const menuButtonInfo = uni.getMenuButtonBoundingClientRect()this.statusBarHeight = systemInfo.statusBarHeightthis.headerHeight = menuButtonInfo.bottom + 8      },      goBack() {        uni.navigateBack()      },      displayContent(message) {if (message.sender === 'me' || !message.id) {return message.content        }if (this.typingMessages[message.id]) {const typingMsg = this.typingMessages[message.id]return typingMsg.content.substring(0, typingMsg.index)        }return message.content      },      sendTip(tipText) {this.inputMessage = tipTextthis.sendMessage()      },      sendMessage() {if (!this.inputMessage.trim()) {return uni.showToast({title'请输入消息',duration1000,icon'none'          })        }this.isSending = trueconst newMessage = {id'msg_' + Date.now(),sender'me',contentthis.inputMessage,timenewDate().getTime()        }this.messages.push(newMessage)const userMessage = this.inputMessagethis.inputMessage = ''this.hideAllPanels()this.$nextTick(() => {this.scrollToBottom(true)        })const typingMessageId = 'msg_typing_' + Date.now()const typingMessage = {id: typingMessageId,sender'other',content'',typingtrue,timenewDate().getTime()        }this.messages.push(typingMessage)this.scrollToBottom(true)this.getAIResponse(userMessage).then(aiResponse => {this.receiveReply(aiResponse, typingMessageId.replace('typing_'''))        }).catch(err => {console.error('AI请求失败:', err)this.messages = this.messages.filter(msg => !msg.typing)this.receiveReply('抱歉,我暂时无法处理您的请求,请稍后再试。')        }).finally(() => {this.isSending = false        })      },      getAIResponse(userMessage) {returnnewPromise((resolve, reject) => {          uni.request({url'https://api.deepseek.com/chat/completions',method'POST',header: {'Authorization''Bearer your key','Content-Type''application/json'            },data: {"model""deepseek-chat","messages": [{"role""user","content": userMessage              }],"stream"false,"max_tokens"2512,"temperature"0.7            },success(res) => {if (res.data && res.data.choices && res.data.choices[0] && res.data                .choices[0].message) {const replyContent = res.data.choices[0].message.contentif (replyContent) {                  resolve(replyContent)                } else {                  reject(newError('AI回复为空'))                }              } else {                reject(newError('无效的API响应'))              }            },fail(err) => {              reject(err)            }          })        })      },      receiveReply(content, messageId) {this.messages = this.messages.filter(msg => !msg.typing)const replyMessage = {id: messageId || ('msg_' + Date.now()),sender'other',content: content,timenewDate().getTime()        }this.messages.push(replyMessage)this.scrollToBottom(true)if (content && content.length > 0) {this.startTypingEffect(replyMessage.id, content)        }      },      startTypingEffect(messageId, fullContent) {this.$set(this.typingMessages, messageId, {content: fullContent,index0,timernull        })const typeNextChar = () => {const typingMsg = this.typingMessages[messageId]if (!typingMsg) returnif (typingMsg.index < typingMsg.content.length) {            typingMsg.index++this.$forceUpdate()this.scrollToBottom(true)            typingMsg.timer = setTimeout(typeNextChar, this.typingSpeed)          } else {this.$delete(this.typingMessages, messageId)          }        }        setTimeout(typeNextChar, 300)      },      scrollToBottom(animate = false) {        setTimeout(() => {const query = uni.createSelectorQuery().in(this)          query.select('.chat-content').boundingClientRect()          query.select('.chat-messages').boundingClientRect()          query.exec(res => {if (res[0] && res[1]) {// 确保滚动到底部this.scrollTop = res[1].height - res[0].height + 100            }          })        }, 100)      },      loadMoreMessages() {if (this.loading) returnthis.loading = true        uni.showLoading({title'加载中...'        })        setTimeout(() => {const oldMessages = [...this.messages]const newMessages = [{id'msg_' + (Date.now() - 86400000),sender'other',content'欢迎使用!有什么问题随时问我~',timeDate.now() - 86400000          }]this.messages = [...newMessages, ...oldMessages]this.loading = false          uni.hideLoading()        }, 800)      },      toggleMorePanel() {this.showMorePanel = !this.showMorePanel      },      hideAllPanels() {this.showMorePanel = false      },      formatTime(timestamp) {const date = newDate(timestamp)const hours = date.getHours().toString().padStart(2'0')const minutes = date.getMinutes().toString().padStart(2'0')return`${hours}:${minutes}`      },// 相册功能      uploadImage() {this.hideAllPanels()        uni.chooseImage({count1,sizeType: ['compressed'],sourceType: ['album'],success(res) => {const tempFilePath = res.tempFilePaths[0]            uni.showLoading({title'发送中...'            })            setTimeout(() => {const newMessage = {id'msg_' + Date.now(),sender'me',imageUrl: tempFilePath,content'[图片]',timenewDate().getTime()              }this.messages.push(newMessage)this.scrollToBottom(true)              uni.hideLoading()              uni.showToast({title'发送成功',icon'success',duration1000              })            }, 500)          },fail(err) => {console.error('选择图片失败', err)            uni.showToast({title'选择图片失败',icon'none'            })          }        })      },// 拍照功能      takePhoto() {this.hideAllPanels()        uni.chooseImage({count1,sizeType: ['compressed'],sourceType: ['camera'],success(res) => {const tempFilePath = res.tempFilePaths[0]            uni.showLoading({title'发送中...'            })            setTimeout(() => {const newMessage = {id'msg_' + Date.now(),sender'me',imageUrl: tempFilePath,content'[拍照]',timenewDate().getTime()              }this.messages.push(newMessage)this.scrollToBottom(true)              uni.hideLoading()              uni.showToast({title'发送成功',icon'success',duration1000              })            }, 500)          },fail(err) => {console.error('拍照失败', err)            uni.showToast({title'拍照失败',icon'none'            })          }        })      },// 预览图片      previewImage(imageUrl) {        uni.previewImage({urls: [imageUrl],current: imageUrl        })      },      startRecord() {        uni.getRecorderManager().onStart(() => {this.isRecording = truethis.recordingTime = 0this.recordingTimer = setInterval(() => {this.recordingTime++if (this.recordingTime >= 60) {this.stopRecord()            }          }, 1000)        })        uni.getRecorderManager().onStop((res) => {this.isRecording = falseif (this.recordingTimer) {            clearInterval(this.recordingTimer)this.recordingTimer = null          }if (this.recordingTime < 1) {            uni.showToast({title'录音时间太短',icon'none'            })return          }this.sendVoiceMessage(res.tempFilePath)        })        uni.getRecorderManager().onError((err) => {console.error('录音错误', err)this.isRecording = falseif (this.recordingTimer) {            clearInterval(this.recordingTimer)this.recordingTimer = null          }          uni.showToast({title'录音失败',icon'none'          })        })        uni.getRecorderManager().start({duration60000,format'mp3'        })      },      stopRecord() {if (this.isRecording) {          uni.getRecorderManager().stop()        }      },      cancelRecord() {if (this.isRecording) {          uni.getRecorderManager().stop()this.isRecording = falseif (this.recordingTimer) {            clearInterval(this.recordingTimer)this.recordingTimer = null          }          uni.showToast({title'已取消录音',icon'none'          })        }      },      sendVoiceMessage(filePath) {const newMessage = {id'msg_' + Date.now(),sender'me',content'[语音消息]',voiceUrl: filePath,durationthis.recordingTime,timenewDate().getTime()        }this.messages.push(newMessage)this.scrollToBottom(true)      },      playVoice(message) {if (!message.voiceUrl) returnconst innerAudioContext = uni.createInnerAudioContext()        innerAudioContext.src = message.voiceUrl        innerAudioContext.onError((err) => {console.error('语音播放错误', err)          uni.showToast({title'播放失败',icon'none'          })          innerAudioContext.destroy()        })        innerAudioContext.play()      }    },    onLoad() {this.getSystemInfo()this.scrollToBottom()    },    beforeDestroy() {Object.values(this.typingMessages).forEach(msg => {if (msg.timer) {          clearTimeout(msg.timer)        }      })    }  }</script><stylelang="scss"scoped>  .chat-container {    height: 100vh;    display: flex;    flex-direction: column;    background: #F1F1FE;    background-attachment: fixed;    background: linear-gradient(to bottom, transparent, #F1F1FE 300px), radial-gradient(20% 150px at 70% 230px, rgb(51 0 255 / 20%), transparent), radial-gradient(40% 180px at 80% 50px, rgb(166 161 243 / 30%), transparent), radial-gradient(50% 300px at 90% 100px, rgba(212, 230, 241, 0.8), transparent), radial-gradient(20% 150px at 0px 0px, rgba(162, 213, 239, 0.5), transparent), radial-gradient(30% 200px at 100px 50px, rgb(167 196 249 / 50%), transparent), #FFF0F5  }  /* 顶部导航栏 - 风格,与胶囊按钮对齐 */  .chat-header {    display: flex;    align-items: center;    justify-content: space-between;    padding-left: 24rpx;    padding-right: 24rpx;    box-sizing: border-box;    position: relative;    z-index: 10;  }  .header-left {    display: flex;    align-items: center;    justify-content: center;  }  .back-icon {    font-size: 48rpx;    color: #5e5ce0;    font-weight: 400;  }  .header-center {    flex: 1;    display: flex;    align-items: center;    justify-content: center;  }  .bot-name {    font-size: 36rpx;    font-weight: 600;    color: #000000;    line-height: 1.4;    margin-right: 20rpx;  }  .status-badge {    display: flex;    align-items: center;    margin-top: 4rpx;  }  .status-dot {    width: 12rpx;    height: 12rpx;    background-color: #4cd964;    border-radius: 50%;    margin-right: 8rpx;  }  .status-text {    font-size: 22rpx;    color: #8e8e93;  }  .header-right {    width: 80rpx;    height: 80rpx;    display: flex;    align-items: center;    justify-content: center;  }  .more-icon {    font-size: 44rpx;    color: #5e5ce0;    font-weight: 600;  }  /* 聊天内容区域 */  .chat-content {    flex: 1;    padding: 20rpx 24rpx;    overflow: hidden;    box-sizing: border-box;    height: auto;  }  .chat-messages {    display: flex;    flex-direction: column;    padding-bottom: 20rpx;  }  .time-divider {    display: flex;    justify-content: center;    margin: 24rpx 0;  }  .time-divider text {    background-color: rgba(0, 0, 0, 0.3);    backdrop-filter: blur(10px);    color: rgba(255, 255, 255, 0.9);    font-size: 22rpx;    padding: 6rpx 20rpx;    border-radius: 20rpx;  }  .message-item {    display: flex;    margin-bottom: 24rpx;    width: 100%;  }  .message-item.my-message {    justify-content: flex-end;  }  .message-item.other-message {    justify-content: flex-start;  }  .message-content {    // max-width: 80%;    display: flex;    flex-direction: column;  }  /* AI消息特殊样式 - 宽度100% */  .ai-bubble {    max-width: 100%;    display: block;  }  .ai-message-text {    display: block;    width: 100%;    word-wrap: break-word;    word-break: break-all;    white-space: pre-wrap;    line-height: 1.6;  }  .message-content.my-content {    align-items: flex-end;  }  .message-bubble {    padding: 20rpx 24rpx;    border-radius: 18rpx;    position: relative;    word-break: break-word;  }  .other-bubble {    background-color: rgba(255, 255, 255, 0.95);    backdrop-filter: blur(10px);    border-top-left-radius: 8rpx;    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);  }  .my-bubble {    background: linear-gradient(135deg, #009688 0%, #03A9F4 100%);    border-top-right-radius: 8rpx;    box-shadow: 0 2rpx 8rpx rgba(94, 92, 224, 0.3);  }  .message-text {    font-size: 28rpx;    line-height: 1.5;    color: #1e1e1e;  }  .my-bubble .message-text {    color: #ffffff;  }  .message-time {    font-size: 22rpx;    color: rgba(255, 255, 255, 0.7);    margin-top: 8rpx;    margin-left: 12rpx;    margin-right: 12rpx;  }  .my-time {    text-align: right;  }  /* 图片消息样式 */  .message-image {    max-width: 400rpx;    max-height: 400rpx;    border-radius: 16rpx;  }  .image-bubble {    padding: 0rpx !important;    background: #00000000 !important;    box-shadow:none !important;  }  /* 语音消息样式 */  .voice-bubble {    display: flex;    align-items: center;    min-width: 140rpx;    padding: 16rpx 24rpx !important;  }  .voice-img {    width: 36rpx;    height: 36rpx;    margin-right: 12rpx;  }  .voice-duration {    font-size: 28rpx;    color: #ffffff;  }  /* 提示词区域 - 固定在输入框上方 */  .tip-wrapper {    background: transparent;    padding: 16rpx 24rpx;    border-top: 1rpx solid rgba(0, 0, 0, 0.05);  }  .tip-scroll {    white-space: nowrap;    width: 100%;  }  .tip-list {    display: inline-flex;    gap: 16rpx;  }  .tip-item {    display: inline-block;    padding: 16rpx 32rpx;    background: rgba(255, 255, 255, 0.95);    border-radius: 48rpx;    font-size: 28rpx;    color: #5e5ce0;    font-weight: 500;    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);    transition: all 0.2s ease;    white-space: nowrap;  }  .tip-item:active {    transform: scale(0.96);    background: rgba(94, 92, 224, 0.1);  }  /* 输入区域 - 风格 */  .input-area {    padding-bottom: env(safe-area-inset-bottom);    position: relative;    z-index: 1000000;    background-color: #fff;  }  .input-container {    display: flex;    align-items: center;    padding: 16rpx 24rpx;    gap: 16rpx;    background-color: #fff;  }  .function-buttons {    display: flex;    gap: 16rpx;  }  .func-btn {    width: 64rpx;    height: 64rpx;    display: flex;    align-items: center;    justify-content: center;    border-radius: 32rpx;    background-color: #f5f5f5;    transition: all 0.2s;  }  .func-img {    width: 40rpx;    height: 40rpx;  }  .func-btn:active {    background-color: rgba(233, 236, 239, 0.9);    transform: scale(0.95);  }  .input-wrapper {    flex: 1;    position: relative;    display: flex;    align-items: center;  }  .message-input {    flex: 1;    background-color: #f5f5f5;    border-radius: 48rpx;    padding: 20rpx 80rpx 20rpx 28rpx;    font-size: 32rpx;    height: 72rpx;    line-height: 72rpx;    box-sizing: border-box;    vertical-align: middle;  }  .placeholder-style {    color: #c6c6c8;    font-size: 28rpx;    line-height: 72rpx;    vertical-align: middle;    display: inline-block;    transform: translateY(0);  }  .voice-input-btn {    position: absolute;    right: 12rpx;    top: 50%;    transform: translateY(-50%);    width: 56rpx;    height: 56rpx;    display: flex;    align-items: center;    justify-content: center;    border-radius: 28rpx;    background-color: transparent;  }  .voice-input-img {    width: 36rpx;    height: 36rpx;  }  .voice-input-btn:active {    background-color: rgba(233, 236, 239, 0.5);  }  .send-btn {    width: 120rpx;    margin: 0;    background-color: #f5f5f5;    color: #8e8e93;    border-radius: 48rpx;    height: 72rpx;    line-height: 72rpx;    font-size: 28rpx;    padding: 0 24rpx;    border: none;    font-weight: 500;    transition: all 0.2s;    border: none !important;    outline: none !important;  }  .send-btn.active {    background: linear-gradient(135deg, #5e5ce0 0%, #7b7ae6 100%);    color: white;  }  .send-btn[disabled] {    opacity: 0.5;  }  /* 录音状态遮罩 */  .recording-mask {    position: fixed;    top: 0;    left: 0;    right: 0;    bottom: 0;    background-color: rgba(0, 0, 0, 0.8);    display: flex;    align-items: center;    justify-content: center;    z-index: 999;  }  .recording-content {    display: flex;    flex-direction: column;    align-items: center;  }  .recording-animation {    display: flex;    align-items: center;    justify-content: center;    margin-bottom: 40rpx;    height: 100rpx;  }  .recording-wave {    width: 12rpx;    height: 40rpx;    background: linear-gradient(135deg, #5e5ce0 0%, #7b7ae6 100%);    margin: 0 8rpx;    border-radius: 12rpx;    animation: wave 1s ease-in-out infinite;  }  .recording-wave:nth-child(1) {    animation-delay: 0s;  }  .recording-wave:nth-child(2) {    animation-delay: 0.2s;  }  .recording-wave:nth-child(3) {    animation-delay: 0.4s;  }  @keyframes wave {    0%,    100% {      height: 40rpx;    }    50% {      height: 80rpx;    }  }  .recording-text {    color: #ffffff;    font-size: 32rpx;    margin-bottom: 20rpx;  }  .recording-tip {    color: rgba(255, 255, 255, 0.7);    font-size: 26rpx;  }  /* 更多功能面板 */  .more-panel {    position: absolute;    bottom: 100%;    left: 0;    right: 0;    background-color: #fff;    border-radius: 24rpx 24rpx 0 0;    animation: slideUp 0.3s ease;    z-index: 999;  }  @keyframes slideUp {    from {      transform: translateY(100%);      opacity: 0;    }    to {      transform: translateY(0);      opacity: 1;    }  }  .more-grid {    display: flex;    align-items: center;    justify-content: flex-start;    padding: 24rpx 32rpx;    gap: 55rpx;  }  .more-item {    display: flex;    flex-direction: column;    align-items: center;    gap: 12rpx;  }  .more-icon-bg {    width: 88rpx;    height: 88rpx;    display: flex;    align-items: center;    justify-content: center;    background-color: #f5f5f5;    border-radius: 44rpx;    transition: all 0.2s;  }  .more-img {    width: 48rpx;    height: 48rpx;  }  .more-item:active .more-icon-bg {    transform: scale(0.95);    background-color: rgba(233, 236, 239, 0.9);  }  .more-text {    font-size: 24rpx;    color: #737373;    font-weight: 500;  }  /* 打字指示器 */  .typing-indicator {    display: flex;    padding: 8rpx 0 0 0;  }  .typing-indicator .dot {    width: 8rpx;    height: 8rpx;    background-color: #5e5ce0;    border-radius: 50%;    margin-right: 8rpx;    animation: typingAnimation 1.4s infinite ease-in-out;  }  .typing-indicator .dot:nth-child(1) {    animation-delay: 0s;  }  .typing-indicator .dot:nth-child(2) {    animation-delay: 0.2s;  }  .typing-indicator .dot:nth-child(3) {    animation-delay: 0.4s;    margin-right: 0;  }  @keyframes typingAnimation {    0%,    60%,    100% {      transform: translateY(0);      opacity: 0.6;    }    30% {      transform: translateY(-10rpx);      opacity: 1;    }  }</style>