VoiceInput.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <template>
  2. <div class="voice-input-container">
  3. <button
  4. @click="toggleSpeechInput"
  5. class="speech-btn"
  6. :class="{ 'recording': isRecording }"
  7. >
  8. <div class="waveform-container" v-if="isRecording">
  9. <LiveWaveform
  10. :active="isRecording"
  11. :processing="false"
  12. :height="25"
  13. :barWidth="2"
  14. :barGap="1"
  15. :barRadius="1"
  16. :sensitivity="1.2"
  17. />
  18. </div>
  19. <el-icon v-if="!isRecording"><Microphone /></el-icon>
  20. <!-- 显示倒计时(仅录音时显示) -->
  21. <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
  22. </button>
  23. </div>
  24. </template>
  25. <script setup>
  26. import { ref, onMounted, onUnmounted } from 'vue'
  27. import { Microphone } from '@element-plus/icons-vue'
  28. import { ElMessage } from 'element-plus'
  29. import LiveWaveform from './LiveWaveform.vue'
  30. // 定义props
  31. const props = defineProps({
  32. // 语音识别语言,默认为中文
  33. lang: {
  34. type: String,
  35. default: 'zh-CN'
  36. },
  37. // 最大录音时间,默认为10秒
  38. maxDuration: {
  39. type: Number,
  40. default: 10
  41. }
  42. })
  43. // 定义emit事件
  44. const emit = defineEmits(['voiceRecognized', 'recordingStatusChanged'])
  45. // 语音输入响应式变量
  46. const isRecording = ref(false) // 录音状态
  47. const recognition = ref(null) // 语音识别实例
  48. const countdown = ref(0) // 倒计时剩余秒数
  49. const countdownTimer = ref(null) // 倒计时定时器
  50. const isBrowserSupported = ref(true) // 浏览器是否支持语音识别
  51. // 检测浏览器是否支持语音识别
  52. const checkBrowserSupport = () => {
  53. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
  54. if (!SpeechRecognition) {
  55. ElMessage.warning('当前浏览器不支持语音识别输入功能')
  56. isBrowserSupported.value = false
  57. return false
  58. }
  59. return true
  60. }
  61. // 初始化语音识别
  62. const initSpeechRecognition = () => {
  63. if (!checkBrowserSupport()) {
  64. return null
  65. }
  66. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
  67. const instance = new SpeechRecognition()
  68. instance.lang = props.lang
  69. instance.interimResults = true
  70. instance.continuous = true
  71. instance.onresult = (event) => {
  72. // 遍历所有结果,包括临时结果
  73. for (let i = event.resultIndex; i < event.results.length; i++) {
  74. const transcript = event.results[i][0].transcript
  75. // 无论是否是最终结果,实时识别结果
  76. emit('voiceRecognized', transcript)
  77. // 打印语音识别结果
  78. console.log('语音输入文字:', transcript)
  79. }
  80. }
  81. // 识别器结束时清除定时器
  82. instance.onend = () => {
  83. clearInterval(countdownTimer.value)
  84. isRecording.value = false
  85. countdown.value = 0
  86. emit('recordingStatusChanged', false)
  87. }
  88. instance.onerror = (event) => {
  89. console.error('语音识别错误:', event.error)
  90. clearInterval(countdownTimer.value) // 出错时清除定时器
  91. isRecording.value = false
  92. emit('recordingStatusChanged', false)
  93. ElMessage.error('语音输入失败,请重试!', true)
  94. countdown.value = 0
  95. }
  96. return instance
  97. }
  98. // 获取浏览器信息
  99. const getBrowserInfo = () => {
  100. const userAgent = window.navigator.userAgent.toLowerCase()
  101. if (userAgent.includes('edg')) {
  102. return { name: 'Edge', version: (userAgent.match(/edg\/([\d.]+)/) || [])[1] }
  103. }
  104. if (userAgent.includes('chrome')) {
  105. return { name: 'Chrome', version: (userAgent.match(/chrome\/([\d.]+)/) || [])[1] }
  106. }
  107. if (userAgent.includes('firefox')) {
  108. return { name: 'Firefox', version: (userAgent.match(/firefox\/([\d.]+)/) || [])[1] }
  109. }
  110. if (userAgent.includes('safari')) {
  111. return { name: 'Safari', version: (userAgent.match(/version\/([\d.]+)/) || [])[1] }
  112. }
  113. return { name: 'Unknown', version: 'Unknown' }
  114. }
  115. // 切换录音状态
  116. const toggleSpeechInput = () => {
  117. const browser = getBrowserInfo()
  118. // 已知Edge支持,Chrome不支持
  119. if (browser.name !== 'Edge') {
  120. ElMessage.error('当前浏览器暂不支持语音输入功能,请使用Edge浏览器')
  121. isBrowserSupported.value = false
  122. return false
  123. }
  124. // 无论当前状态如何,先清除可能存在的旧定时器
  125. clearInterval(countdownTimer.value)
  126. countdownTimer.value = null
  127. if (isRecording.value) {
  128. // 手动停止时立即重置状态,确保在所有浏览器中波纹都能立即关闭
  129. isRecording.value = false
  130. countdown.value = 0
  131. emit('recordingStatusChanged', false)
  132. recognition.value?.stop()
  133. } else {
  134. // 初始化倒计时前再次清除定时器(防止快速点击)
  135. clearInterval(countdownTimer.value)
  136. countdown.value = props.maxDuration // 设置最大录音时间
  137. recognition.value = initSpeechRecognition()
  138. if (!recognition.value) return
  139. navigator.mediaDevices.getUserMedia({ audio: true })
  140. .then(() => {
  141. recognition.value.start()
  142. isRecording.value = true
  143. emit('recordingStatusChanged', true)
  144. // 启动新的倒计时定时器
  145. countdownTimer.value = setInterval(() => {
  146. countdown.value--
  147. if (countdown.value <= 0) {
  148. clearInterval(countdownTimer.value) // 倒计时结束清除
  149. recognition.value.stop()
  150. }
  151. }, 1000)
  152. })
  153. .catch((err) => {
  154. console.error('麦克风权限获取失败:', err)
  155. ElMessage.warning('请允许麦克风权限以使用语音输入')
  156. // 出错时重置状态
  157. isRecording.value = false
  158. emit('recordingStatusChanged', false)
  159. countdown.value = 0
  160. })
  161. }
  162. }
  163. // 组件卸载时清理资源
  164. onUnmounted(() => {
  165. clearInterval(countdownTimer.value)
  166. recognition.value?.stop()
  167. })
  168. </script>
  169. <style scoped lang="scss">
  170. @use 'sass:math';
  171. // 定义rpx转换函数
  172. @function rpx($px) {
  173. @return math.div($px, 750) * 100vw;
  174. }
  175. .voice-input-container {
  176. display: flex;
  177. flex-direction: column;
  178. align-items: center;
  179. gap: rpx(8);
  180. }
  181. .speech-btn {
  182. padding: rpx(5) rpx(10);
  183. background: #fff;
  184. border: 1px solid #ffce1b;
  185. border-radius: rpx(5);
  186. cursor: pointer;
  187. display: flex;
  188. align-items: center;
  189. gap: rpx(4);
  190. // 取消点击后的边框
  191. &:focus,
  192. &:focus-visible {
  193. outline: none;
  194. box-shadow: none;
  195. }
  196. &.recording {
  197. background: #ffeeba;
  198. border-color: #ffc107;
  199. .el-icon {
  200. color: #dc3545;
  201. }
  202. }
  203. .el-icon {
  204. font-size: rpx(8);
  205. color: #666;
  206. }
  207. }
  208. .waveform-container {
  209. // width: 100%;
  210. max-width: rpx(30);
  211. }
  212. .countdown-text {
  213. font-size: rpx(6);
  214. color: #666;
  215. }
  216. </style>