|
|
@@ -0,0 +1,166 @@
|
|
|
+<template>
|
|
|
+ <button
|
|
|
+ @click="toggleSpeechInput"
|
|
|
+ class="speech-btn"
|
|
|
+ :class="{ 'recording': isRecording }"
|
|
|
+ >
|
|
|
+ <el-icon v-if="!isRecording"><Microphone /></el-icon>
|
|
|
+ <el-icon v-else><Mute /></el-icon>
|
|
|
+ <!-- 显示倒计时(仅录音时显示) -->
|
|
|
+ <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
|
|
|
+ </button>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
+import { Microphone, Mute } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+// 定义props
|
|
|
+const props = defineProps({
|
|
|
+ // 语音识别语言,默认为中文
|
|
|
+ lang: {
|
|
|
+ type: String,
|
|
|
+ default: 'zh-CN'
|
|
|
+ },
|
|
|
+ // 最大录音时间,默认为10秒
|
|
|
+ maxDuration: {
|
|
|
+ type: Number,
|
|
|
+ default: 10
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 定义emit事件
|
|
|
+const emit = defineEmits(['voiceRecognized', 'recordingStatusChanged'])
|
|
|
+
|
|
|
+// 语音输入响应式变量
|
|
|
+const isRecording = ref(false) // 录音状态
|
|
|
+const recognition = ref(null) // 语音识别实例
|
|
|
+const countdown = ref(0) // 倒计时剩余秒数
|
|
|
+const countdownTimer = ref(null) // 倒计时定时器
|
|
|
+
|
|
|
+// 初始化语音识别
|
|
|
+const initSpeechRecognition = () => {
|
|
|
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
|
+ if (!SpeechRecognition) {
|
|
|
+ ElMessage.warning('当前浏览器不支持语音输入功能')
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const instance = new SpeechRecognition()
|
|
|
+ instance.lang = props.lang
|
|
|
+ instance.interimResults = false
|
|
|
+
|
|
|
+ instance.onresult = (event) => {
|
|
|
+ if (event.results?.[0]?.[0]) {
|
|
|
+ emit('voiceRecognized', event.results[0][0].transcript)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 识别器结束时清除定时器
|
|
|
+ instance.onend = () => {
|
|
|
+ clearInterval(countdownTimer.value)
|
|
|
+ isRecording.value = false
|
|
|
+ countdown.value = 0
|
|
|
+ emit('recordingStatusChanged', false)
|
|
|
+ }
|
|
|
+
|
|
|
+ instance.onerror = (event) => {
|
|
|
+ console.error('语音识别错误:', event.error)
|
|
|
+ clearInterval(countdownTimer.value) // 出错时清除定时器
|
|
|
+ isRecording.value = false
|
|
|
+ emit('recordingStatusChanged', false)
|
|
|
+ ElMessage.error('语音输入失败,请重试!', true)
|
|
|
+ countdown.value = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return instance
|
|
|
+}
|
|
|
+
|
|
|
+// 切换录音状态
|
|
|
+const toggleSpeechInput = () => {
|
|
|
+ // 无论当前状态如何,先清除可能存在的旧定时器
|
|
|
+ clearInterval(countdownTimer.value)
|
|
|
+ countdownTimer.value = null
|
|
|
+
|
|
|
+ if (isRecording.value) {
|
|
|
+ // 手动停止时重置状态
|
|
|
+ countdown.value = 0
|
|
|
+ recognition.value?.stop()
|
|
|
+ isRecording.value = false
|
|
|
+ emit('recordingStatusChanged', false)
|
|
|
+ } else {
|
|
|
+ // 初始化倒计时前再次清除定时器(防止快速点击)
|
|
|
+ clearInterval(countdownTimer.value)
|
|
|
+ countdown.value = props.maxDuration // 设置最大录音时间
|
|
|
+
|
|
|
+ recognition.value = initSpeechRecognition()
|
|
|
+ if (!recognition.value) return
|
|
|
+
|
|
|
+ navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
+ .then(() => {
|
|
|
+ recognition.value.start()
|
|
|
+ isRecording.value = true
|
|
|
+ emit('recordingStatusChanged', true)
|
|
|
+
|
|
|
+ // 启动新的倒计时定时器
|
|
|
+ countdownTimer.value = setInterval(() => {
|
|
|
+ countdown.value--
|
|
|
+ if (countdown.value <= 0) {
|
|
|
+ clearInterval(countdownTimer.value) // 倒计时结束清除
|
|
|
+ recognition.value.stop()
|
|
|
+ isRecording.value = false
|
|
|
+ emit('recordingStatusChanged', false)
|
|
|
+ countdown.value = 0
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error('麦克风权限获取失败:', err)
|
|
|
+ ElMessage.warning('请允许麦克风权限以使用语音输入')
|
|
|
+ // 出错时重置状态
|
|
|
+ isRecording.value = false
|
|
|
+ emit('recordingStatusChanged', false)
|
|
|
+ countdown.value = 0
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 组件卸载时清理资源
|
|
|
+onUnmounted(() => {
|
|
|
+ clearInterval(countdownTimer.value)
|
|
|
+ recognition.value?.stop()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+@use 'sass:math';
|
|
|
+// 定义rpx转换函数
|
|
|
+@function rpx($px) {
|
|
|
+ @return math.div($px, 750) * 100vw;
|
|
|
+}
|
|
|
+.speech-btn {
|
|
|
+ padding: rpx(5) rpx(10);
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ffce1b;
|
|
|
+ border-radius: rpx(5);
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: rpx(4);
|
|
|
+
|
|
|
+ &.recording {
|
|
|
+ background: #ffeeba;
|
|
|
+ border-color: #ffc107;
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ color: #dc3545;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: rpx(8);
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|