mi-assessment/uniapp/components/VoiceRecorder/index.vue
2026-02-09 14:45:06 +08:00

244 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>