851 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			851 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | ||
|   <view class="chat-container">
 | ||
|     <!-- 顶部导航栏 -->
 | ||
|     <view class="top_bar flex w-full" v-if="isMobile">
 | ||
|       <view class="chat-header">
 | ||
|         <view class="header-left" @click="goBack">
 | ||
|           <i class="fas fa-arrow-left"></i>
 | ||
|         </view>
 | ||
|         <view class="header-title">聊天</view>
 | ||
|         <view class="header-right" @click="goToChatDetail">
 | ||
|           <i class="fas fa-ellipsis-v"></i>
 | ||
|         </view>
 | ||
|       </view>
 | ||
|     </view>
 | ||
|     
 | ||
|     <!-- 浏览器环境下的导航栏 -->
 | ||
|     <view class="chat-header" v-else>
 | ||
|       <view class="header-left" @click="goBack">
 | ||
|         <i class="fas fa-arrow-left"></i>
 | ||
|       </view>
 | ||
|       <view class="header-title">聊天</view>
 | ||
|       <view class="header-right" @click="goToChatDetail">
 | ||
|         <i class="fas fa-ellipsis-v"></i>
 | ||
|       </view>
 | ||
|     </view>
 | ||
| 
 | ||
|     <!-- 聊天消息列表 -->
 | ||
|     <scroll-view
 | ||
|       class="message-list"
 | ||
|       scroll-y
 | ||
|       :scroll-top="scrollTop"
 | ||
|       :scroll-into-view="scrollIntoView"
 | ||
|     >
 | ||
|       <block v-for="(message, index) in messages" :key="index">
 | ||
|         <view
 | ||
|           :id="'message-' + index"
 | ||
|           :class="[
 | ||
|             'message-item',
 | ||
|             message.type === 'sent' ? 'sent' : 'received',
 | ||
|           ]"
 | ||
|         >
 | ||
|           <!-- 头像 -->
 | ||
|           <view class="avatar" @click="goToUserDetail">
 | ||
|             <image
 | ||
|               :src="
 | ||
|                 message.type === 'sent'
 | ||
|                   ? '/static/imgs/default_avatar.png'
 | ||
|                   : '/static/imgs/default_avatar.png'
 | ||
|               "
 | ||
|               mode="aspectFill"
 | ||
|             ></image>
 | ||
|           </view>
 | ||
| 
 | ||
|           <!-- 消息内容 -->
 | ||
|           <view class="message-content">
 | ||
|             <view class="message-bubble">
 | ||
|               <!-- 文本消息 -->
 | ||
|               <text class="message-text">{{
 | ||
|                 parseEmoji(message.content)
 | ||
|               }}</text>
 | ||
|             </view>
 | ||
|             <!-- 消息时间 -->
 | ||
|             <view class="message-time" v-if="message.time">
 | ||
|               {{ formatTime(message.time) }}
 | ||
|             </view>
 | ||
|           </view>
 | ||
|         </view>
 | ||
|       </block>
 | ||
|     </scroll-view>
 | ||
| 
 | ||
|     <!-- 输入区域 -->
 | ||
|     <view class="input-area">
 | ||
|       <!-- 表情选择器 -->
 | ||
|       <view class="emoji-picker-container" v-if="showEmojiPicker">
 | ||
|         <EmojiPicker
 | ||
|           :visible="showEmojiPicker"
 | ||
|           @select="onEmojiSelect"
 | ||
|           @close="showEmojiPicker = false"
 | ||
|         />
 | ||
|       </view>
 | ||
| 
 | ||
|       <!-- 输入工具栏 -->
 | ||
|       <view class="input-toolbar">
 | ||
|         <!-- 左侧切换按钮 -->
 | ||
|         <view class="left-switch">
 | ||
|           <view
 | ||
|             class="switch-btn"
 | ||
|             @click="switchInputMode"
 | ||
|             :class="{ active: inputMode === 'voice' }"
 | ||
|           >
 | ||
|             <i
 | ||
|               :class="
 | ||
|                 inputMode === 'voice' ? 'fas fa-keyboard' : 'fas fa-microphone'
 | ||
|               "
 | ||
|             ></i>
 | ||
|           </view>
 | ||
|         </view>
 | ||
| 
 | ||
|         <!-- 中间输入区域 -->
 | ||
|         <view class="input-wrapper">
 | ||
|           <!-- 语音模式:按住说话按钮 -->
 | ||
|           <view
 | ||
|             v-if="inputMode === 'voice'"
 | ||
|             class="voice-input"
 | ||
|             :class="{ recording: isRecording }"
 | ||
|             @touchstart="startVoiceRecord"
 | ||
|             @touchend="endVoiceRecord"
 | ||
|             @touchcancel="cancelVoiceRecord"
 | ||
|           >
 | ||
|             <text class="voice-text">{{
 | ||
|               isRecording ? "松开结束" : "按住说话"
 | ||
|             }}</text>
 | ||
|             <view v-if="isRecording" class="recording-indicator">
 | ||
|               <view class="recording-dot"></view>
 | ||
|               <view class="recording-dot"></view>
 | ||
|               <view class="recording-dot"></view>
 | ||
|             </view>
 | ||
|           </view>
 | ||
| 
 | ||
|           <!-- 文本模式:输入框 -->
 | ||
|           <view v-else class="text-input-container">
 | ||
|             <textarea
 | ||
|               v-model="inputMessage"
 | ||
|               placeholder="输入消息..."
 | ||
|               class="message-input"
 | ||
|               :style="{ height: inputHeight + 'rpx' }"
 | ||
|               :maxlength="500"
 | ||
|               @confirm="sendMessage"
 | ||
|               @focus="onInputFocus"
 | ||
|               @blur="onInputBlur"
 | ||
|               @input="onInputChange"
 | ||
|               @linechange="onLineChange"
 | ||
|             />
 | ||
|           </view>
 | ||
|         </view>
 | ||
| 
 | ||
|         <!-- 右侧操作区 -->
 | ||
|         <view class="right-actions">
 | ||
|           <!-- 文本模式:表情按钮 -->
 | ||
|           <view
 | ||
|             v-if="inputMode === 'text'"
 | ||
|             class="emoji-btn"
 | ||
|             @click="toggleEmojiPicker"
 | ||
|             :class="{ active: showEmojiPicker }"
 | ||
|           >
 | ||
|             <i class="fas fa-smile"></i>
 | ||
|           </view>
 | ||
| 
 | ||
|           <!-- 文本模式:发送按钮(有内容时显示) -->
 | ||
|           <view
 | ||
|             v-if="inputMode === 'text' && inputMessage.trim()"
 | ||
|             class="send-btn"
 | ||
|             @click="sendMessage"
 | ||
|           >
 | ||
|             发送
 | ||
|           </view>
 | ||
|         </view>
 | ||
|       </view>
 | ||
|     </view>
 | ||
|   </view>
 | ||
| </template>
 | ||
| 
 | ||
| <script>
 | ||
| import EmojiPicker from "../../src/components/EmojiPicker.vue";
 | ||
| import { parseEmoji } from "../../src/utils/emojiParser.js";
 | ||
| 
 | ||
| export default {
 | ||
|   components: {
 | ||
|     EmojiPicker,
 | ||
|   },
 | ||
|   data() {
 | ||
|     return {
 | ||
|       messages: [
 | ||
|         {
 | ||
|           type: "received",
 | ||
|           content: "欢迎使用聊天功能!有什么可以帮您?",
 | ||
|           time: new Date(Date.now() - 3000 * 60 * 1000), // 30分钟前
 | ||
|         },
 | ||
|         {
 | ||
|           type: "received",
 | ||
|           content: "您可以咨询产品信息、下单流程等相关问题。",
 | ||
|           time: new Date(Date.now() - 29 * 60 * 1000), // 29分钟前
 | ||
|         },
 | ||
|         {
 | ||
|           type: "sent",
 | ||
|           content: "请问你们有哪些热门产品?",
 | ||
|           time: new Date(Date.now() - 28 * 60 * 1000), // 28分钟前
 | ||
|         },
 | ||
|         {
 | ||
|           type: "received",
 | ||
|           content: "我们的热门产品有A、B、C三款,您想了解哪一款?",
 | ||
|           time: new Date(Date.now() - 27 * 60 * 1000), // 27分钟前
 | ||
|         },
 | ||
|         {
 | ||
|           type: "received",
 | ||
|           content: "点击发送按钮或按回车键发送消息哦~",
 | ||
|           time: new Date(Date.now() - 26 * 60 * 1000), // 26分钟前
 | ||
|         },
 | ||
|       ],
 | ||
|       inputMessage: "",
 | ||
|       showEmojiPicker: false,
 | ||
|       inputMode: "text", // 'text' 或 'voice'
 | ||
|       isRecording: false,
 | ||
|       recordingTimer: null,
 | ||
|       inputHeight: 80, // 输入框高度,默认1行
 | ||
|       scrollTop: 0, // 消息列表滚动位置
 | ||
|       scrollIntoView: "", // 滚动到指定元素
 | ||
|     };
 | ||
|   },
 | ||
|   mounted() {
 | ||
|     // 确保输入框初始高度为1行
 | ||
|     this.inputHeight = 80;
 | ||
|   },
 | ||
|   computed: {
 | ||
|     // 从全局数据获取设备信息
 | ||
|     isMobile() {
 | ||
|       return getApp().globalData.isMobile;
 | ||
|     }
 | ||
|   },
 | ||
|   watch: {
 | ||
|     // 监听消息数组变化,自动滚动到最新消息
 | ||
|     messages: {
 | ||
|       handler(newMessages, oldMessages) {
 | ||
|         if (newMessages.length > (oldMessages ? oldMessages.length : 0)) {
 | ||
|           // 有新消息时,延迟滚动确保DOM更新完成
 | ||
|           this.$nextTick(() => {
 | ||
|             setTimeout(() => {
 | ||
|               this.scrollToBottom();
 | ||
|             }, 200);
 | ||
|           });
 | ||
|         }
 | ||
|       },
 | ||
|       deep: true,
 | ||
|       immediate: false,
 | ||
|     },
 | ||
|   },
 | ||
|   methods: {
 | ||
|     parseEmoji(text) {
 | ||
|       // 使用工具函数解析emoji
 | ||
|       return parseEmoji(text);
 | ||
|     },
 | ||
|     sendMessage() {
 | ||
|       if (this.inputMessage.trim() !== "") {
 | ||
|         // 添加用户发送的消息
 | ||
|         const newMessage = {
 | ||
|           type: "sent",
 | ||
|           content: this.inputMessage,
 | ||
|           time: new Date(),
 | ||
|         };
 | ||
|         this.messages.push(newMessage);
 | ||
| 
 | ||
|         // 强制更新视图
 | ||
|         this.$forceUpdate();
 | ||
| 
 | ||
|         // 清空输入框并重置高度
 | ||
|         const message = this.inputMessage;
 | ||
|         this.inputMessage = "";
 | ||
|         this.inputHeight = 80; // 重置为1行高度
 | ||
|         this.showEmojiPicker = false;
 | ||
| 
 | ||
|         // 立即滚动到最新消息
 | ||
|         this.$nextTick(() => {
 | ||
|           this.scrollToBottom();
 | ||
|         });
 | ||
| 
 | ||
|         // 模拟回复
 | ||
|         setTimeout(() => {
 | ||
|           this.simulateReply(message);
 | ||
|         }, 1000);
 | ||
|       }
 | ||
|     },
 | ||
|     simulateReply(message) {
 | ||
|       // 简单的回复逻辑
 | ||
|       let reply = "感谢你的消息!";
 | ||
|       if (message.includes("你好")) {
 | ||
|         reply = "你好!很高兴见到你!";
 | ||
|       } else if (message.includes("产品")) {
 | ||
|         reply = "我们的产品非常棒,你可以查看我们的官网了解更多信息。";
 | ||
|       }
 | ||
| 
 | ||
|       const replyMessage = {
 | ||
|         type: "received",
 | ||
|         content: reply,
 | ||
|         time: new Date(),
 | ||
|       };
 | ||
| 
 | ||
|       this.messages.push(replyMessage);
 | ||
| 
 | ||
|       // 强制更新视图
 | ||
|       this.$forceUpdate();
 | ||
| 
 | ||
|       // 立即滚动到最新消息
 | ||
|       this.$nextTick(() => {
 | ||
|         this.scrollToBottom();
 | ||
|       });
 | ||
|     },
 | ||
|     toggleEmojiPicker() {
 | ||
|       this.showEmojiPicker = !this.showEmojiPicker;
 | ||
|     },
 | ||
|     onEmojiSelect(emoji) {
 | ||
|       this.inputMessage += emoji.unicode;
 | ||
|     },
 | ||
|     onInputFocus() {
 | ||
|       this.showEmojiPicker = false;
 | ||
|     },
 | ||
|     onInputBlur() {
 | ||
|       // 输入框失焦时的处理
 | ||
|     },
 | ||
|     onInputChange(e) {
 | ||
|       // 输入内容变化时的处理
 | ||
|       this.inputMessage = e.detail.value;
 | ||
| 
 | ||
|       // 根据内容长度估算行数并调整高度
 | ||
|       this.adjustInputHeight();
 | ||
|     },
 | ||
|     onLineChange(e) {
 | ||
|       // 行数变化时调整高度,限制最大10行
 | ||
|       const lineCount = e.detail.lineCount;
 | ||
|       const minHeight = 80; // 1行高度
 | ||
|       const lineHeight = 40; // 每行高度
 | ||
|       const maxLines = 10; // 最大10行
 | ||
| 
 | ||
|       let newHeight = minHeight + (lineCount - 1) * lineHeight;
 | ||
|       newHeight = Math.min(
 | ||
|         Math.max(newHeight, minHeight),
 | ||
|         minHeight + (maxLines - 1) * lineHeight
 | ||
|       );
 | ||
| 
 | ||
|       this.inputHeight = newHeight;
 | ||
|     },
 | ||
|     adjustInputHeight() {
 | ||
|       // 根据输入内容调整高度
 | ||
|       const content = this.inputMessage;
 | ||
|       if (!content.trim()) {
 | ||
|         // 如果内容为空,设置为1行高度
 | ||
|         this.inputHeight = 80;
 | ||
|         return;
 | ||
|       }
 | ||
| 
 | ||
|       const lines = content.split("\n").length;
 | ||
|       const minHeight = 80;
 | ||
|       const lineHeight = 40;
 | ||
|       const maxLines = 10;
 | ||
| 
 | ||
|       let newHeight = minHeight + (lines - 1) * lineHeight;
 | ||
|       newHeight = Math.min(
 | ||
|         Math.max(newHeight, minHeight),
 | ||
|         minHeight + (maxLines - 1) * lineHeight
 | ||
|       );
 | ||
| 
 | ||
|       this.inputHeight = newHeight;
 | ||
|     },
 | ||
|     // 切换输入模式
 | ||
|     switchInputMode() {
 | ||
|       if (this.inputMode === "text") {
 | ||
|         this.inputMode = "voice";
 | ||
|         this.showEmojiPicker = false;
 | ||
|       } else {
 | ||
|         this.inputMode = "text";
 | ||
|       }
 | ||
|     },
 | ||
|     // 切换更多选项
 | ||
|     toggleMoreOptions() {
 | ||
|       // 这里可以添加更多选项的弹窗
 | ||
|     },
 | ||
|     // 语音录制相关
 | ||
|     startVoiceRecord() {
 | ||
|       this.isRecording = true;
 | ||
|       // 这里可以添加实际的录音逻辑
 | ||
|       // 例如调用 uni.getRecorderManager()
 | ||
|     },
 | ||
|     endVoiceRecord() {
 | ||
|       this.isRecording = false;
 | ||
|       // 这里可以添加录音结束的处理逻辑
 | ||
|       // 例如发送语音消息
 | ||
|     },
 | ||
|     cancelVoiceRecord() {
 | ||
|       this.isRecording = false;
 | ||
|       // 这里可以添加取消录音的处理逻辑
 | ||
|     },
 | ||
|     scrollToBottom() {
 | ||
|       // 滚动到消息列表底部
 | ||
|       // 方法1:使用scroll-top
 | ||
|       this.scrollTop = 99999;
 | ||
| 
 | ||
|       // 方法2:使用scroll-into-view滚动到最后一个消息
 | ||
|       if (this.messages.length > 0) {
 | ||
|         const lastIndex = this.messages.length - 1;
 | ||
|         this.scrollIntoView = "message-" + lastIndex;
 | ||
| 
 | ||
|         // 重置scrollIntoView以允许重复滚动
 | ||
|         setTimeout(() => {
 | ||
|           this.scrollIntoView = "";
 | ||
|         }, 100);
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * 返回
 | ||
|      */
 | ||
|     goBack() {
 | ||
|       uni.navigateBack();
 | ||
|     },
 | ||
|     /**
 | ||
|      * 跳转到聊天详情
 | ||
|      */
 | ||
|     goToChatDetail() {
 | ||
|       uni.navigateTo({
 | ||
|         url: "/pages/message/chatdetail",
 | ||
|       });
 | ||
|     },
 | ||
|     /**
 | ||
|      * 跳转到用户详情
 | ||
|      */
 | ||
|     goToUserDetail() {
 | ||
|       uni.navigateTo({
 | ||
|         url: "/pages/message/userdetail",
 | ||
|       });
 | ||
|     },
 | ||
|     /**
 | ||
|      * 格式化时间
 | ||
|      */
 | ||
|     formatTime(time) {
 | ||
|       const date = new Date(time);
 | ||
|       const now = new Date();
 | ||
| 
 | ||
|       // 如果是今天,只显示时间
 | ||
|       if (date.toDateString() === now.toDateString()) {
 | ||
|         return date.toLocaleTimeString("zh-CN", {
 | ||
|           hour: "2-digit",
 | ||
|           minute: "2-digit",
 | ||
|         });
 | ||
|       }
 | ||
| 
 | ||
|       // 如果是昨天,显示"昨天"
 | ||
|       const yesterday = new Date(now);
 | ||
|       yesterday.setDate(yesterday.getDate() - 1);
 | ||
|       if (date.toDateString() === yesterday.toDateString()) {
 | ||
|         return (
 | ||
|           "昨天 " +
 | ||
|           date.toLocaleTimeString("zh-CN", {
 | ||
|             hour: "2-digit",
 | ||
|             minute: "2-digit",
 | ||
|           })
 | ||
|         );
 | ||
|       }
 | ||
| 
 | ||
|       // 其他情况显示完整日期
 | ||
|       return (
 | ||
|         date.toLocaleDateString("zh-CN") +
 | ||
|         " " +
 | ||
|         date.toLocaleTimeString("zh-CN", {
 | ||
|           hour: "2-digit",
 | ||
|           minute: "2-digit",
 | ||
|         })
 | ||
|       );
 | ||
|     },
 | ||
|   },
 | ||
| };
 | ||
| </script>
 | ||
| 
 | ||
| <style scoped>
 | ||
| .chat-container {
 | ||
|   display: flex;
 | ||
|   flex-direction: column;
 | ||
|   height: 100vh;
 | ||
|   background-color: var(--background);
 | ||
| }
 | ||
| 
 | ||
| /* 移动设备顶部状态栏 */
 | ||
| .top_bar {
 | ||
|   background: var(--gradient-primary);
 | ||
|   box-shadow: var(--shadow-lg);
 | ||
|   z-index: 9999;
 | ||
|   position: fixed;
 | ||
|   top: 0;
 | ||
|   left: 0;
 | ||
|   right: 0;
 | ||
|   height: calc(var(--status-bar-height) + 88rpx);
 | ||
|   display: flex;
 | ||
|   align-items: flex-end;
 | ||
|   padding-top: var(--status-bar-height);
 | ||
|   box-sizing: border-box;
 | ||
| }
 | ||
| 
 | ||
| /* 支持安全区域的设备 */
 | ||
| @supports (padding: max(0px)) {
 | ||
|   .top_bar {
 | ||
|     height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
 | ||
|     padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
 | ||
|   }
 | ||
|   
 | ||
|   .top_bar + .message-list {
 | ||
|     margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /* 顶部导航栏 */
 | ||
| .chat-header {
 | ||
|   display: flex;
 | ||
|   justify-content: space-between;
 | ||
|   align-items: center;
 | ||
|   height: 88rpx;
 | ||
|   background-color: var(--surface);
 | ||
|   border-bottom: 1rpx solid var(--border);
 | ||
|   padding: 0 20rpx;
 | ||
|   box-sizing: border-box;
 | ||
|   box-shadow: var(--shadow);
 | ||
| }
 | ||
| 
 | ||
| /* 移动设备下的导航栏样式 */
 | ||
| .top_bar .chat-header {
 | ||
|   background: transparent;
 | ||
|   border-bottom: none;
 | ||
|   box-shadow: none;
 | ||
|   width: 100%;
 | ||
|   height: 88rpx;
 | ||
|   padding: 0 20rpx;
 | ||
|   box-sizing: border-box;
 | ||
| }
 | ||
| 
 | ||
| .top_bar .header-title {
 | ||
|   color: var(--white);
 | ||
|   font-weight: 600;
 | ||
|   font-size: 36rpx;
 | ||
| }
 | ||
| 
 | ||
| .top_bar .header-left i,
 | ||
| .top_bar .header-right i {
 | ||
|   color: var(--white);
 | ||
|   font-size: 40rpx;
 | ||
| }
 | ||
| 
 | ||
| .header-left,
 | ||
| .header-right {
 | ||
|   width: 80rpx;
 | ||
|   height: 88rpx;
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
|   transition: background-color 0.2s ease;
 | ||
| }
 | ||
| 
 | ||
| .header-right:active {
 | ||
|   background-color: var(--surface-hover);
 | ||
|   border-radius: 8rpx;
 | ||
| }
 | ||
| 
 | ||
| /* 移动设备下的按钮悬停效果 */
 | ||
| .top_bar .header-left:active,
 | ||
| .top_bar .header-right:active {
 | ||
|   background-color: rgba(255, 255, 255, 0.2);
 | ||
|   border-radius: 8rpx;
 | ||
| }
 | ||
| 
 | ||
| .header-title {
 | ||
|   font-size: 32rpx;
 | ||
|   font-weight: 600;
 | ||
|   color: var(--title-color);
 | ||
| }
 | ||
| 
 | ||
| .iconfont {
 | ||
|   font-family: "iconfont" !important;
 | ||
|   font-size: 36rpx;
 | ||
|   font-style: normal;
 | ||
|   color: var(--text-secondary);
 | ||
| }
 | ||
| 
 | ||
| /* 消息列表 */
 | ||
| .message-list {
 | ||
|   flex: 1;
 | ||
|   padding: 20rpx;
 | ||
|   overflow-y: auto;
 | ||
|   background-color: var(--background);
 | ||
| }
 | ||
| 
 | ||
| /* 移动设备下为消息列表添加顶部间距 */
 | ||
| .top_bar + .message-list {
 | ||
|   margin-top: calc(var(--status-bar-height) + 88rpx);
 | ||
| }
 | ||
| 
 | ||
| .message-item {
 | ||
|   display: flex;
 | ||
|   margin-bottom: 40rpx;
 | ||
| }
 | ||
| 
 | ||
| .message-item.sent {
 | ||
|   flex-direction: row-reverse;
 | ||
| }
 | ||
| 
 | ||
| /* 头像 */
 | ||
| .avatar {
 | ||
|   width: 80rpx;
 | ||
|   height: 80rpx;
 | ||
|   border-radius: 10rpx;
 | ||
|   overflow: hidden;
 | ||
|   flex-shrink: 0;
 | ||
|   margin: 0 20rpx;
 | ||
| }
 | ||
| 
 | ||
| .avatar image {
 | ||
|   width: 100%;
 | ||
|   height: 100%;
 | ||
| }
 | ||
| 
 | ||
| /* 消息内容 */
 | ||
| .message-content {
 | ||
|   display: flex;
 | ||
|   flex-direction: column;
 | ||
|   max-width: 70%;
 | ||
| }
 | ||
| 
 | ||
| .message-item.sent .message-content {
 | ||
|   align-items: flex-end;
 | ||
| }
 | ||
| 
 | ||
| .message-item.received .message-content {
 | ||
|   align-items: flex-start;
 | ||
| }
 | ||
| 
 | ||
| /* 消息气泡 */
 | ||
| .message-bubble {
 | ||
|   position: relative;
 | ||
|   padding: 20rpx;
 | ||
|   border-radius: 12rpx;
 | ||
|   word-wrap: break-word;
 | ||
|   word-break: break-all;
 | ||
|   font-size: 28rpx;
 | ||
|   line-height: 1.4;
 | ||
|   max-width: 100%;
 | ||
| }
 | ||
| 
 | ||
| .message-item.sent .message-bubble {
 | ||
|   background: var(--gradient-primary);
 | ||
|   color: var(--white);
 | ||
|   border-radius: 12rpx 2rpx 12rpx 12rpx;
 | ||
| }
 | ||
| 
 | ||
| .message-item.received .message-bubble {
 | ||
|   background-color: var(--surface);
 | ||
|   color: var(--text-color);
 | ||
|   border-radius: 2rpx 12rpx 12rpx 12rpx;
 | ||
|   box-shadow: var(--shadow);
 | ||
| }
 | ||
| 
 | ||
| /* 消息时间 */
 | ||
| .message-time {
 | ||
|   font-size: 20rpx;
 | ||
|   color: var(--text-muted);
 | ||
|   margin-top: 10rpx;
 | ||
| }
 | ||
| 
 | ||
| /* 输入区域 */
 | ||
| .input-area {
 | ||
|   background-color: var(--surface);
 | ||
|   border-top: 1rpx solid var(--border);
 | ||
|   box-shadow: var(--shadow-md);
 | ||
| }
 | ||
| 
 | ||
| /* 表情选择器容器 */
 | ||
| .emoji-picker-container {
 | ||
|   border-bottom: 1rpx solid var(--border-light);
 | ||
| }
 | ||
| 
 | ||
| /* 输入工具栏 */
 | ||
| .input-toolbar {
 | ||
|   display: flex;
 | ||
|   align-items: flex-end;
 | ||
|   padding: 20rpx 16rpx;
 | ||
|   gap: 16rpx;
 | ||
|   min-height: 100rpx;
 | ||
| }
 | ||
| 
 | ||
| /* 左侧切换按钮 */
 | ||
| .left-switch {
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
| }
 | ||
| 
 | ||
| .switch-btn {
 | ||
|   width: 80rpx;
 | ||
|   height: 80rpx;
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
|   border-radius: 50%;
 | ||
|   background-color: transparent;
 | ||
|   transition: all 0.2s ease;
 | ||
| }
 | ||
| 
 | ||
| .switch-btn.active {
 | ||
|   background: var(--gradient-primary);
 | ||
| }
 | ||
| 
 | ||
| .switch-btn i {
 | ||
|   font-size: 40rpx;
 | ||
|   color: var(--text-secondary);
 | ||
| }
 | ||
| 
 | ||
| .switch-btn.active i {
 | ||
|   color: var(--white);
 | ||
| }
 | ||
| 
 | ||
| /* 中间输入区域 */
 | ||
| .input-wrapper {
 | ||
|   flex: 1;
 | ||
|   position: relative;
 | ||
| }
 | ||
| 
 | ||
| /* 文本输入容器 */
 | ||
| .text-input-container {
 | ||
|   background-color: var(--surface);
 | ||
|   border-radius: 8rpx;
 | ||
|   border: 1rpx solid var(--border);
 | ||
|   overflow: hidden;
 | ||
|   box-shadow: var(--shadow);
 | ||
| }
 | ||
| 
 | ||
| .message-input {
 | ||
|   width: 100%;
 | ||
|   height: 80rpx; /* 固定默认高度为1行 */
 | ||
|   min-height: 80rpx;
 | ||
|   max-height: 440rpx; /* 10行高度:80rpx + 9 * 40rpx = 440rpx */
 | ||
|   padding: 20rpx 24rpx;
 | ||
|   border: none;
 | ||
|   font-size: 32rpx;
 | ||
|   line-height: 1.4;
 | ||
|   background-color: transparent;
 | ||
|   box-sizing: border-box;
 | ||
|   resize: none;
 | ||
|   word-wrap: break-word;
 | ||
|   word-break: break-all;
 | ||
|   overflow-y: auto; /* 超出10行时显示滚动条 */
 | ||
| }
 | ||
| 
 | ||
| .message-input:focus {
 | ||
|   outline: none;
 | ||
| }
 | ||
| 
 | ||
| /* 语音输入 */
 | ||
| .voice-input {
 | ||
|   width: 100%;
 | ||
|   height: 80rpx;
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
|   background-color: var(--gray-lighter);
 | ||
|   border-radius: 8rpx;
 | ||
|   transition: all 0.2s ease;
 | ||
|   position: relative;
 | ||
|   border: 1rpx solid var(--border);
 | ||
| }
 | ||
| 
 | ||
| .voice-input:active,
 | ||
| .voice-input.recording {
 | ||
|   background-color: var(--gray);
 | ||
| }
 | ||
| 
 | ||
| .voice-text {
 | ||
|   font-size: 32rpx;
 | ||
|   color: var(--text-secondary);
 | ||
| }
 | ||
| 
 | ||
| .recording-indicator {
 | ||
|   position: absolute;
 | ||
|   right: 20rpx;
 | ||
|   display: flex;
 | ||
|   gap: 8rpx;
 | ||
| }
 | ||
| 
 | ||
| .recording-dot {
 | ||
|   width: 12rpx;
 | ||
|   height: 12rpx;
 | ||
|   background-color: #ff4444;
 | ||
|   border-radius: 50%;
 | ||
|   animation: recordingPulse 1s infinite;
 | ||
| }
 | ||
| 
 | ||
| .recording-dot:nth-child(2) {
 | ||
|   animation-delay: 0.2s;
 | ||
| }
 | ||
| 
 | ||
| .recording-dot:nth-child(3) {
 | ||
|   animation-delay: 0.4s;
 | ||
| }
 | ||
| 
 | ||
| @keyframes recordingPulse {
 | ||
|   0%,
 | ||
|   100% {
 | ||
|     opacity: 0.3;
 | ||
|     transform: scale(0.8);
 | ||
|   }
 | ||
|   50% {
 | ||
|     opacity: 1;
 | ||
|     transform: scale(1.2);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /* 右侧操作区 */
 | ||
| .right-actions {
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   gap: 16rpx;
 | ||
| }
 | ||
| 
 | ||
| .emoji-btn {
 | ||
|   width: 80rpx;
 | ||
|   height: 80rpx;
 | ||
|   display: flex;
 | ||
|   align-items: center;
 | ||
|   justify-content: center;
 | ||
|   border-radius: 50%;
 | ||
|   background-color: transparent;
 | ||
|   transition: all 0.2s ease;
 | ||
| }
 | ||
| 
 | ||
| .emoji-btn.active {
 | ||
|   background: var(--gradient-primary);
 | ||
| }
 | ||
| 
 | ||
| .emoji-btn:active {
 | ||
|   background-color: var(--surface-hover);
 | ||
| }
 | ||
| 
 | ||
| .emoji-btn i {
 | ||
|   font-size: 40rpx;
 | ||
|   color: var(--text-secondary);
 | ||
| }
 | ||
| 
 | ||
| .emoji-btn.active i {
 | ||
|   color: var(--white);
 | ||
| }
 | ||
| 
 | ||
| .send-btn {
 | ||
|   padding: 16rpx 32rpx;
 | ||
|   background: var(--gradient-primary);
 | ||
|   color: var(--white);
 | ||
|   border-radius: 8rpx;
 | ||
|   font-size: 28rpx;
 | ||
|   font-weight: 500;
 | ||
|   transition: all 0.2s ease;
 | ||
|   min-width: 120rpx;
 | ||
|   text-align: center;
 | ||
|   box-shadow: var(--shadow);
 | ||
| }
 | ||
| 
 | ||
| .send-btn:active {
 | ||
|   background: var(--primary-dark);
 | ||
|   transform: scale(0.98);
 | ||
| }
 | ||
| </style>
 |