xiangyixiangqin/miniapp/聊天功能增强指南.md
2026-01-14 20:23:13 +08:00

11 KiB
Raw Blame History

聊天功能增强指南 - 表情和语音消息

创建日期: 2026-01-14
功能: 表情消息 + 语音消息


📋 已创建的文件

1. 表情功能

  • miniapp/utils/emoji.js - 表情数据300+ 表情)
  • miniapp/components/EmojiPicker/index.vue - 表情选择器组件

2. 语音功能

  • miniapp/components/VoiceRecorder/index.vue - 语音录制组件
  • miniapp/api/chat.js - 添加了 uploadVoice() API

🔧 集成步骤

步骤 1: 在聊天页面引入组件

miniapp/pages/chat/index.vue<script setup> 部分添加:

import EmojiPicker from '@/components/EmojiPicker/index.vue'
import VoiceRecorder from '@/components/VoiceRecorder/index.vue'
import { uploadVoice } from '@/api/chat.js'

步骤 2: 添加状态变量

// 输入模式text | voice
const inputMode = ref('text')

// 表情选择器显示状态
const showEmojiPicker = ref(false)

步骤 3: 修改底部操作栏 HTML

将现有的底部操作栏替换为:

<!-- 底部操作栏 -->
<view class="bottom-action-bar">
  <!-- 三个操作按钮 - 键盘弹起时隐藏 -->
  <view class="action-buttons" v-show="!isInputFocused && inputMode === 'text'">
    <button class="action-btn" @click="handleExchangeWeChat">交换微信</button>
    <button class="action-btn" @click="handleCall">拨打电话</button>
    <button class="action-btn" @click="handleExchangePhoto">交换照片</button>
  </view>
  
  <!-- 输入区域 -->
  <view class="input-area">
    <!-- 切换语音/文本按钮 -->
    <view class="mode-switch-btn" @click="handleSwitchInputMode">
      <text class="icon">{{ inputMode === 'text' ? '🎤' : '⌨️' }}</text>
    </view>
    
    <!-- 文本输入模式 -->
    <view v-if="inputMode === 'text'" class="text-input-wrapper">
      <input 
        v-model="inputText"
        class="message-input"
        type="text"
        placeholder="请输入..."
        placeholder-style="color: #999;"
        :adjust-position="true"
        confirm-type="send"
        @focus="handleInputFocus"
        @blur="handleInputBlur"
        @confirm="handleSendMessage"
      />
      
      <!-- 表情按钮 -->
      <view class="emoji-btn" @click="handleToggleEmoji">
        <text class="icon">😊</text>
      </view>
    </view>
    
    <!-- 语音输入模式 -->
    <VoiceRecorder 
      v-else 
      class="voice-input-wrapper"
      @send="handleSendVoice"
    />
    
    <!-- 发送按钮 -->
    <button 
      v-if="inputMode === 'text'"
      class="send-btn" 
      :class="{ active: inputText.trim() }" 
      @click="handleSendMessage"
    >
      发送
    </button>
  </view>
</view>

<!-- 表情选择器 -->
<EmojiPicker 
  :visible="showEmojiPicker"
  @close="showEmojiPicker = false"
  @select="handleEmojiSelect"
/>

步骤 4: 添加事件处理函数

// 切换输入模式
const handleSwitchInputMode = () => {
  inputMode.value = inputMode.value === 'text' ? 'voice' : 'text'
  showEmojiPicker.value = false
}

// 切换表情选择器
const handleToggleEmoji = () => {
  showEmojiPicker.value = !showEmojiPicker.value
}

// 选择表情
const handleEmojiSelect = (emoji) => {
  inputText.value += emoji
  showEmojiPicker.value = false
}

// 发送语音消息
const handleSendVoice = async (voiceData) => {
  try {
    // 显示上传中
    uni.showLoading({ title: '发送中...' })
    
    // 上传语音文件
    const uploadRes = await uploadVoice(voiceData.tempFilePath)
    
    if (uploadRes && uploadRes.code === 0) {
      const voiceUrl = uploadRes.data.url
      
      // 创建本地消息
      const localMessage = {
        id: Date.now(),
        sessionId: sessionId.value,
        senderId: myUserId.value,
        receiverId: targetUserId.value,
        messageType: MessageType.VOICE,
        voiceUrl: voiceUrl,
        voiceDuration: voiceData.duration,
        status: MessageStatus.SENDING,
        createTime: new Date().toISOString(),
        isMine: true
      }
      
      messages.value.push(localMessage)
      await nextTick()
      scrollToBottom()
      
      // 发送消息
      const res = await sendMessage({
        sessionId: sessionId.value,
        receiverId: targetUserId.value,
        messageType: MessageType.VOICE,
        voiceUrl: voiceUrl,
        voiceDuration: voiceData.duration
      })
      
      uni.hideLoading()
      
      if (res && res.code === 0) {
        localMessage.status = MessageStatus.SENT
        if (res.data && res.data.messageId) {
          localMessage.id = res.data.messageId
        }
      } else {
        localMessage.status = MessageStatus.FAILED
      }
    } else {
      uni.hideLoading()
      uni.showToast({ title: '上传失败', icon: 'none' })
    }
  } catch (error) {
    uni.hideLoading()
    console.error('发送语音失败:', error)
    uni.showToast({ title: '发送失败', icon: 'none' })
  }
}

步骤 5: 在消息列表中显示语音消息

在消息列表的模板中添加语音消息类型:

<!-- 语音消息 -->
<view 
  v-else-if="message.messageType === MessageType.VOICE" 
  class="bubble voice-bubble"
  @click="handlePlayVoice(message)"
>
  <view class="voice-content">
    <text class="voice-icon">🎤</text>
    <text class="voice-duration">{{ message.voiceDuration }}"</text>
    <view class="voice-wave" :class="{ playing: playingVoiceId === message.id }">
      <view class="wave-bar"></view>
      <view class="wave-bar"></view>
      <view class="wave-bar"></view>
    </view>
  </view>
</view>

步骤 6: 添加语音播放功能

// 当前播放的语音ID
const playingVoiceId = ref(null)
const innerAudioContext = ref(null)

// 播放语音
const handlePlayVoice = (message) => {
  if (!message.voiceUrl) return
  
  // 如果正在播放同一条语音,则停止
  if (playingVoiceId.value === message.id) {
    stopVoice()
    return
  }
  
  // 停止之前的播放
  stopVoice()
  
  // 创建音频上下文
  if (!innerAudioContext.value) {
    innerAudioContext.value = uni.createInnerAudioContext()
    
    innerAudioContext.value.onEnded(() => {
      playingVoiceId.value = null
    })
    
    innerAudioContext.value.onError((err) => {
      console.error('语音播放失败:', err)
      playingVoiceId.value = null
      uni.showToast({ title: '播放失败', icon: 'none' })
    })
  }
  
  // 播放语音
  innerAudioContext.value.src = message.voiceUrl
  innerAudioContext.value.play()
  playingVoiceId.value = message.id
}

// 停止播放
const stopVoice = () => {
  if (innerAudioContext.value) {
    innerAudioContext.value.stop()
  }
  playingVoiceId.value = null
}

// 页面卸载时停止播放
onUnmounted(() => {
  stopVoice()
  if (innerAudioContext.value) {
    innerAudioContext.value.destroy()
  }
})

步骤 7: 添加样式

// 底部操作栏样式
.bottom-action-bar {
  .input-area {
    display: flex;
    align-items: center;
    gap: 16rpx;
    padding: 16rpx 24rpx;
    background-color: #fff;
    
    .mode-switch-btn {
      width: 64rpx;
      height: 64rpx;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: #f5f5f5;
      border-radius: 50%;
      
      .icon {
        font-size: 40rpx;
      }
    }
    
    .text-input-wrapper {
      flex: 1;
      display: flex;
      align-items: center;
      gap: 16rpx;
      background-color: #f5f5f5;
      border-radius: 32rpx;
      padding: 0 24rpx;
      
      .message-input {
        flex: 1;
        height: 64rpx;
        font-size: 28rpx;
      }
      
      .emoji-btn {
        width: 48rpx;
        height: 48rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        
        .icon {
          font-size: 36rpx;
        }
      }
    }
    
    .voice-input-wrapper {
      flex: 1;
    }
  }
}

// 语音消息气泡样式
.voice-bubble {
  min-width: 200rpx;
  padding: 24rpx 32rpx;
  
  .voice-content {
    display: flex;
    align-items: center;
    gap: 16rpx;
    
    .voice-icon {
      font-size: 32rpx;
    }
    
    .voice-duration {
      font-size: 28rpx;
      color: #333;
    }
    
    .voice-wave {
      display: flex;
      align-items: center;
      gap: 6rpx;
      
      .wave-bar {
        width: 4rpx;
        height: 20rpx;
        background-color: #999;
        border-radius: 2rpx;
        
        &:nth-child(2) {
          height: 30rpx;
        }
        
        &:nth-child(3) {
          height: 24rpx;
        }
      }
      
      &.playing {
        .wave-bar {
          animation: wave 0.8s ease-in-out infinite;
          
          &:nth-child(2) {
            animation-delay: 0.2s;
          }
          
          &:nth-child(3) {
            animation-delay: 0.4s;
          }
        }
      }
    }
  }
}

@keyframes wave {
  0%, 100% {
    transform: scaleY(1);
  }
  50% {
    transform: scaleY(1.5);
  }
}

🎯 MessageType 枚举更新

确保在 miniapp/store/chat.js 中的 MessageType 包含语音类型:

export const MessageType = {
  TEXT: 1,      // 文本
  VOICE: 2,     // 语音
  IMAGE: 3,     // 图片
  EXCHANGE_WECHAT: 4,  // 交换微信请求
  EXCHANGE_PHOTO: 6    // 交换照片请求
}

🔨 后端 API 需要实现

1. 语音上传接口

路径: POST /api/app/upload/voice

请求: multipart/form-data

  • file: 语音文件mp3 格式)

响应:

{
  "code": 0,
  "message": "上传成功",
  "data": {
    "url": "https://xxx.com/voice/xxx.mp3"
  }
}

2. 发送消息接口更新

确保 POST /api/app/chat/send 支持语音消息类型:

请求体:

{
  "sessionId": 123,
  "receiverId": 456,
  "messageType": 2,
  "voiceUrl": "https://xxx.com/voice/xxx.mp3",
  "voiceDuration": 5
}

功能清单

表情消息

  • 表情选择器组件
  • 300+ 表情数据
  • 5 个分类(常用、笑脸、手势、爱心、符号)
  • 点击发送表情
  • 表情显示在消息中

语音消息

  • 按住说话录音
  • 录音时长显示
  • 最长 60 秒限制
  • 最短 1 秒限制
  • 语音上传
  • 语音播放
  • 播放动画效果
  • 语音时长显示

📝 测试清单

表情功能测试

  • 点击表情按钮,弹出表情选择器
  • 切换表情分类
  • 选择表情,插入到输入框
  • 发送包含表情的消息
  • 对方收到表情消息正常显示

语音功能测试

  • 切换到语音模式
  • 按住录音,显示录音界面
  • 录音时长正常计时
  • 松开发送,语音上传成功
  • 语音消息显示在列表中
  • 点击播放语音
  • 播放动画正常
  • 对方实时收到语音消息
  • 对方可以正常播放

🎉 完成后效果

  1. 表情消息: 用户可以在聊天中发送丰富的表情,增强表达能力
  2. 语音消息: 用户可以发送语音消息,更方便快捷的沟通
  3. 实时推送: 表情和语音消息都通过 SignalR 实时推送
  4. 用户体验: 符合现代即时通讯应用的标准功能

注意:

  1. 语音上传需要后端实现对应的 API 接口
  2. 语音文件建议存储到云存储(腾讯云 COS 或阿里云 OSS
  3. 确保小程序有录音权限(在 manifest.json 中配置)