11 KiB
11 KiB
聊天功能增强指南 - 表情和语音消息
创建日期: 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 秒限制
- ✅ 语音上传
- ✅ 语音播放
- ✅ 播放动画效果
- ✅ 语音时长显示
📝 测试清单
表情功能测试
- 点击表情按钮,弹出表情选择器
- 切换表情分类
- 选择表情,插入到输入框
- 发送包含表情的消息
- 对方收到表情消息正常显示
语音功能测试
- 切换到语音模式
- 按住录音,显示录音界面
- 录音时长正常计时
- 松开发送,语音上传成功
- 语音消息显示在列表中
- 点击播放语音
- 播放动画正常
- 对方实时收到语音消息
- 对方可以正常播放
🎉 完成后效果
- 表情消息: 用户可以在聊天中发送丰富的表情,增强表达能力
- 语音消息: 用户可以发送语音消息,更方便快捷的沟通
- 实时推送: 表情和语音消息都通过 SignalR 实时推送
- 用户体验: 符合现代即时通讯应用的标准功能
注意:
- 语音上传需要后端实现对应的 API 接口
- 语音文件建议存储到云存储(腾讯云 COS 或阿里云 OSS)
- 确保小程序有录音权限(在 manifest.json 中配置)