244 lines
5.1 KiB
Vue
244 lines
5.1 KiB
Vue
<template>
|
||
<view class="voice-recorder">
|
||
<!-- 按住说话按钮 -->
|
||
<view
|
||
class="voice-btn"
|
||
:class="{ recording: isRecording }"
|
||
@touchstart="handleTouchStart"
|
||
@touchend="handleTouchEnd"
|
||
@touchcancel="handleTouchCancel"
|
||
>
|
||
<text>{{ isRecording ? '松开发送' : '按住说话' }}</text>
|
||
</view>
|
||
|
||
<!-- 录音提示弹窗 -->
|
||
<view class="voice-modal" v-if="isRecording">
|
||
<view class="voice-modal-content">
|
||
<view class="voice-icon">
|
||
<view class="voice-wave" :class="{ active: isRecording }"></view>
|
||
<text class="icon">🎤</text>
|
||
</view>
|
||
<text class="voice-tip">{{ recordingTip }}</text>
|
||
<text class="voice-time">{{ recordingTime }}s</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onUnmounted } from 'vue'
|
||
|
||
const emit = defineEmits(['send'])
|
||
|
||
const isRecording = ref(false)
|
||
const recordingTime = ref(0)
|
||
const recordingTip = ref('正在录音...')
|
||
const recorderManager = ref(null)
|
||
const recordingTimer = ref(null)
|
||
const tempFilePath = ref('')
|
||
|
||
// 初始化录音管理器
|
||
const initRecorder = () => {
|
||
recorderManager.value = uni.getRecorderManager()
|
||
|
||
// 录音开始
|
||
recorderManager.value.onStart(() => {
|
||
console.log('[VoiceRecorder] 录音开始')
|
||
isRecording.value = true
|
||
recordingTime.value = 0
|
||
recordingTip.value = '正在录音...'
|
||
|
||
// 开始计时
|
||
recordingTimer.value = setInterval(() => {
|
||
recordingTime.value++
|
||
|
||
// 最长60秒
|
||
if (recordingTime.value >= 60) {
|
||
handleTouchEnd()
|
||
}
|
||
}, 1000)
|
||
})
|
||
|
||
// 录音结束
|
||
recorderManager.value.onStop((res) => {
|
||
console.log('[VoiceRecorder] 录音结束:', res)
|
||
clearInterval(recordingTimer.value)
|
||
|
||
tempFilePath.value = res.tempFilePath
|
||
const duration = Math.ceil(res.duration / 1000)
|
||
|
||
// 录音时长小于1秒,提示太短
|
||
if (duration < 1) {
|
||
uni.showToast({
|
||
title: '录音时间太短',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 发送语音消息
|
||
emit('send', {
|
||
tempFilePath: tempFilePath.value,
|
||
duration: duration
|
||
})
|
||
})
|
||
|
||
// 录音错误
|
||
recorderManager.value.onError((err) => {
|
||
console.error('[VoiceRecorder] 录音错误:', err)
|
||
clearInterval(recordingTimer.value)
|
||
isRecording.value = false
|
||
|
||
uni.showToast({
|
||
title: '录音失败',
|
||
icon: 'none'
|
||
})
|
||
})
|
||
}
|
||
|
||
// 开始录音
|
||
const handleTouchStart = () => {
|
||
if (!recorderManager.value) {
|
||
initRecorder()
|
||
}
|
||
|
||
// 开始录音
|
||
recorderManager.value.start({
|
||
duration: 60000, // 最长60秒
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 48000,
|
||
format: 'mp3'
|
||
})
|
||
}
|
||
|
||
// 结束录音
|
||
const handleTouchEnd = () => {
|
||
if (!isRecording.value) return
|
||
|
||
// 停止录音
|
||
recorderManager.value.stop()
|
||
isRecording.value = false
|
||
}
|
||
|
||
// 取消录音
|
||
const handleTouchCancel = () => {
|
||
if (!isRecording.value) return
|
||
|
||
clearInterval(recordingTimer.value)
|
||
isRecording.value = false
|
||
recordingTip.value = '录音已取消'
|
||
|
||
// 停止录音但不发送
|
||
recorderManager.value.stop()
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
if (recordingTimer.value) {
|
||
clearInterval(recordingTimer.value)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.voice-recorder {
|
||
.voice-btn {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
background-color: #fff;
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
user-select: none;
|
||
|
||
&.recording {
|
||
background-color: #ff6b6b;
|
||
border-color: #ff6b6b;
|
||
color: #fff;
|
||
}
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.voice-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
|
||
.voice-modal-content {
|
||
width: 300rpx;
|
||
height: 300rpx;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.voice-icon {
|
||
position: relative;
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 24rpx;
|
||
|
||
.voice-wave {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
|
||
&.active {
|
||
animation: wave 1.5s ease-out infinite;
|
||
}
|
||
}
|
||
|
||
.icon {
|
||
font-size: 80rpx;
|
||
z-index: 1;
|
||
}
|
||
}
|
||
|
||
.voice-tip {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.voice-time {
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes wave {
|
||
0% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: scale(1.5);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
</style>
|