| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- <template>
- <div class="voice-input-container">
- <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>
- <div class="waveform-container" v-if="isRecording">
- <LiveWaveform
- :active="isRecording"
- :processing="false"
- :height="25"
- :barWidth="2"
- :barGap="1"
- :barRadius="1"
- :sensitivity="1.2"
- />
- </div>
- </button>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted } from 'vue'
- import { Microphone, Mute } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import LiveWaveform from './LiveWaveform.vue'
- // 定义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 isBrowserSupported = ref(true) // 浏览器是否支持语音识别
- // 检测浏览器是否支持语音识别
- const checkBrowserSupport = () => {
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
- if (!SpeechRecognition) {
- ElMessage.warning('当前浏览器不支持语音识别输入功能')
- isBrowserSupported.value = false
- return false
- }
- return true
- }
- // 初始化语音识别
- const initSpeechRecognition = () => {
- if (!checkBrowserSupport()) {
- return null
- }
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
- const instance = new SpeechRecognition()
- instance.lang = props.lang
- instance.interimResults = true
- // instance.continuous = true
- instance.onresult = (event) => {
- // 遍历所有结果,包括临时结果
- for (let i = event.resultIndex; i < event.results.length; i++) {
- const transcript = event.results[i][0].transcript
- // 无论是否是最终结果,实时识别结果
- emit('voiceRecognized', transcript)
- // 打印语音识别结果
- console.log('语音输入文字:', transcript)
- }
- }
- // 识别器真正开始监听时触发
- instance.onstart = () => {
- console.log('语音识别已开始监听')
- }
- // 识别器结束时清除定时器
- 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 getBrowserInfo = () => {
- const userAgent = window.navigator.userAgent.toLowerCase()
- if (userAgent.includes('edg')) {
- return { name: 'Edge', version: (userAgent.match(/edg\/([\d.]+)/) || [])[1] }
- }
- if (userAgent.includes('chrome')) {
- return { name: 'Chrome', version: (userAgent.match(/chrome\/([\d.]+)/) || [])[1] }
- }
- if (userAgent.includes('firefox')) {
- return { name: 'Firefox', version: (userAgent.match(/firefox\/([\d.]+)/) || [])[1] }
- }
- if (userAgent.includes('safari')) {
- return { name: 'Safari', version: (userAgent.match(/version\/([\d.]+)/) || [])[1] }
- }
- return { name: 'Unknown', version: 'Unknown' }
- }
- // 切换录音状态
- const toggleSpeechInput = () => {
- const browser = getBrowserInfo()
- // 已知Edge支持,Chrome不支持
- if (browser.name !== 'Edge') {
- ElMessage.error('当前浏览器暂不支持语音输入功能,请使用Edge浏览器')
- isBrowserSupported.value = false
- return false
- }
- // 无论当前状态如何,先清除可能存在的旧定时器
- clearInterval(countdownTimer.value)
- countdownTimer.value = null
- if (isRecording.value) {
- // 手动停止时立即重置状态,确保在所有浏览器中波纹都能立即关闭
- isRecording.value = false
- countdown.value = 0
- emit('recordingStatusChanged', false)
- recognition.value?.stop()
- } else {
- // 初始化倒计时前再次清除定时器(防止快速点击)
- clearInterval(countdownTimer.value)
- countdown.value = props.maxDuration // 设置最大录音时间
- recognition.value = initSpeechRecognition()
- if (!recognition.value) return
- // 重新绑定onstart事件,确保系统就绪后才开始计时
- recognition.value.onstart = () => {
- console.log('语音识别已开始监听')
- // 系统真正就绪后才启动倒计时
- countdownTimer.value = setInterval(() => {
- countdown.value--
- if (countdown.value <= 0) {
- clearInterval(countdownTimer.value) // 倒计时结束清除
- recognition.value.stop()
- }
- }, 1000)
- }
- navigator.mediaDevices.getUserMedia({ audio: true })
- .then(() => {
- // 先设置UI状态,让用户知道系统正在准备
- isRecording.value = true
- emit('recordingStatusChanged', true)
- // 启动语音识别
- recognition.value.start()
- })
- .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;
- }
- .voice-input-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: rpx(8);
- }
- .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);
- // 取消点击后的边框
- &:focus,
- &:focus-visible {
- outline: none;
- box-shadow: none;
- }
- &.recording {
- background: #ffeeba;
- border-color: #ffc107;
- .el-icon {
- color: #dc3545;
- }
- }
- .el-icon {
- font-size: rpx(8);
- color: #666;
- }
- }
- .waveform-container {
- // width: 100%;
- max-width: rpx(30);
- }
- .countdown-text {
- font-size: rpx(6);
- color: #666;
- }
- </style>
|