VoiceInput.vue 6.9 KB

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