فهرست منبع

Merge branch 'muzi' of http://59.110.91.129:3000/zhangmengying/AIClass into wanzi

丸子 1 ماه پیش
والد
کامیت
6e74e34088

+ 1 - 1
src/components/ai/text/TextToText.vue

@@ -80,7 +80,7 @@ import { teacherList } from '@/api/teachers.js'
 // 导入全局状态
 import { globalState } from "@/utils/globalState.js";
 
-import VoiceInput from '../voice/VoiceInput.vue'
+import VoiceInput from '../voice/VoiceInput2.vue'
 
 
 // 终止按钮

+ 522 - 0
src/components/ai/voice/VoiceInput2.vue

@@ -0,0 +1,522 @@
+<template>
+  <div class="voice-input-container">
+    <button
+        @click="toggleSpeechInput"
+        class="speech-btn"
+        :class="{ 'recording': isRecording }"
+    >
+      <div class="waveform-container" v-if="isRecording">
+        <LiveWaveform
+            style="min-width: 50px;"
+            :active="isRecording"
+            :processing="false"
+            :height="25"
+            :barWidth="2"
+            :barGap="1"
+            :barRadius="1"
+            :sensitivity="1.2"
+        />
+      </div>
+      <el-icon v-if="!isRecording"><Microphone /></el-icon>
+      <!-- 显示倒计时(仅录音时显示) -->
+      <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
+    </button>
+
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { Microphone } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import LiveWaveform from './LiveWaveform.vue'
+import axios from '@/utils/request'
+
+// 定义props
+const props = defineProps({
+  // 语音识别语言,默认为中文
+  lang: {
+    type: String,
+    default: 'zh-CN'
+  },
+  // 最大录音时间,默认为10秒 
+  maxDuration: {
+    type: Number,
+    default: 10
+  },
+  // 输入框选择器,用于获取输入框元素
+  inputSelector: {
+    type: String,
+    default: 'input[type="text"]'
+  }
+})
+
+// 定义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 mediaStream = ref(null) // 媒体流引用,用于释放资源
+const recordingStartText = ref('') // 录音开始时的原始文本
+const recordingStartCursorPos = ref(0) // 录音开始时的光标位置
+const sessionId = ref('session_' + Date.now()) // 会话ID
+const resultPollingInterval = ref(null) // 结果轮询定时器
+const audioContext = ref(null) // 音频上下文
+const mediaStreamSource = ref(null) // 媒体流源
+const scriptProcessor = ref(null) // 脚本处理器
+
+// 检测浏览器是否支持语音识别
+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.maxAlternatives = 1//返回的最大候选结果数
+  instance.continuous = true//收录音频时是否连续识别
+
+  instance.onresult = (event) => {
+    // 遍历所有结果,包括临时结果
+    let fullTranscript = ''
+    for (let i = 0; i < event.results.length; i++) {
+      fullTranscript += event.results[i][0].transcript
+    }
+    // 只在录音状态下发送识别结果
+    if (isRecording.value) {
+      // 计算处理后的文本
+      let processedText = ''
+      let cursorPos = 0
+      if (recordingStartCursorPos.value >= recordingStartText.value.length) {
+        // 光标位置在文本末尾或找不到输入框时,追加到末尾
+        processedText = recordingStartText.value + fullTranscript
+        cursorPos = recordingStartText.value.length + fullTranscript.length
+      } else {
+        // 光标位置在文本中间时,插入到光标位置
+        processedText = recordingStartText.value.substring(0, recordingStartCursorPos.value) + fullTranscript + recordingStartText.value.substring(recordingStartCursorPos.value)
+        cursorPos = recordingStartCursorPos.value + fullTranscript.length
+      }
+      // 无论是否是最终结果,实时识别结果
+      emit('voiceRecognized', {
+        originalText: fullTranscript,
+        processedText: processedText,
+        cursorPos: cursorPos
+      })
+      // 打印语音识别结果
+      console.log('语音输入文字:', fullTranscript)
+    }
+  }
+
+  // 识别器真正开始监听时触发
+  instance.onstart = () => {
+    console.log('语音识别已开始监听')
+  }
+
+  // 识别器结束时清除定时器
+  instance.onend = () => {
+    clearInterval(countdownTimer.value)
+    isRecording.value = false
+    countdown.value = 0
+    emit('recordingStatusChanged', false)
+    // 释放媒体流资源
+    if (mediaStream.value) {
+      mediaStream.value.getTracks().forEach(track => track.stop())
+      mediaStream.value = null
+    }
+  }
+
+  instance.onerror = (event) => {
+    console.error('语音识别错误:', event.error)
+    clearInterval(countdownTimer.value) // 出错时清除定时器
+    isRecording.value = false
+    emit('recordingStatusChanged', false)
+    ElMessage.error('语音输入失败,请重试!', true)
+    countdown.value = 0
+    // 释放媒体流资源
+    if (mediaStream.value) {
+      mediaStream.value.getTracks().forEach(track => track.stop())
+      mediaStream.value = null
+    }
+  }
+
+  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 startBackendRecognition = async () => {
+  try {
+    const startResult = await axios({
+      url: 'admin/speech/recognition/start?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: {}
+    });
+    return startResult;
+  } catch (error) {
+    console.error('启动后端识别服务失败:', error);
+    ElMessage.error('启动语音识别服务失败');
+    return null;
+  }
+}
+
+// 停止后端识别服务
+const stopBackendRecognition = async () => {
+  try {
+    const stopResult = await axios({
+      url: 'admin/speech/recognition/stop?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: {}
+    });
+    return stopResult;
+  } catch (error) {
+    console.error('停止后端识别服务失败:', error);
+    return null;
+  }
+}
+
+// 发送音频数据到后端
+const sendAudioData = async (arrayBuffer) => {
+  try {
+    const result = await axios({
+      url: 'admin/speech/recognition/send?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: arrayBuffer,
+      headers: {
+        'Content-Type': 'application/octet-stream'
+      }
+    });
+    return result;
+  } catch (error) {
+    console.error('发送音频数据失败:', error);
+    return null;
+  }
+}
+
+// 启动轮询获取识别结果
+const startResultPolling = () => {
+  // 每300ms获取一次结果
+  resultPollingInterval.value = setInterval(async () => {
+    try {
+      const result = await axios({
+        url: 'admin/speech/recognition/result?sessionId=' + sessionId.value,
+        method: 'GET'
+      });
+
+      if (result.status === 'success' && result.data && result.data.result) {
+        // 计算处理后的文本
+        let processedText = ''
+        let cursorPos = 0
+        if (recordingStartCursorPos.value >= recordingStartText.value.length) {
+          // 光标位置在文本末尾或找不到输入框时,追加到末尾
+          processedText = recordingStartText.value + result.data.result
+          cursorPos = recordingStartText.value.length + result.data.result.length
+        } else {
+          // 光标位置在文本中间时,插入到光标位置
+          processedText = recordingStartText.value.substring(0, recordingStartCursorPos.value) + result.data.result + recordingStartText.value.substring(recordingStartCursorPos.value)
+          cursorPos = recordingStartCursorPos.value + result.data.result.length
+        }
+        // 发送识别结果
+        emit('voiceRecognized', {
+          originalText: result.data.result,
+          processedText: processedText,
+          cursorPos: cursorPos
+        })
+      }
+    } catch (error) {
+      console.error('获取识别结果失败:', error);
+    }
+  }, 300);
+};
+
+// 停止轮询
+const stopResultPolling = () => {
+  if (resultPollingInterval.value) {
+    clearInterval(resultPollingInterval.value);
+    resultPollingInterval.value = null;
+  }
+};
+
+// 记录录音开始时的输入框状态
+const recordInputState = () => {
+  const input = document.querySelector(props.inputSelector)
+  if (input) {
+    recordingStartCursorPos.value = input.selectionStart
+    recordingStartText.value = input.value
+  } else {
+    // 找不到输入框时,默认在最后面追加
+    recordingStartCursorPos.value = 999999
+    recordingStartText.value = ''
+  }
+}
+
+// 切换录音状态
+const toggleSpeechInput = async () => {
+  // 检查浏览器兼容性
+  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+    ElMessage.error('当前浏览器不支持语音录制功能,请使用现代浏览器如Chrome、Firefox或Edge')
+    isBrowserSupported.value = false
+    return
+  }
+  
+  // 检查是否在安全上下文(HTTPS或localhost)
+  if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
+    ElMessage.error('语音录制功能需要在安全上下文(HTTPS或localhost)中运行')
+    return
+  }
+
+  // 无论当前状态如何,先清除可能存在的旧定时器
+  clearInterval(countdownTimer.value)
+  countdownTimer.value = null
+  stopResultPolling()
+
+  if (isRecording.value) {
+    // 手动停止时立即重置状态,确保在所有浏览器中波纹都能立即关闭
+    isRecording.value = false
+    countdown.value = 0
+    emit('recordingStatusChanged', false)
+    
+    // 停止音频处理
+    if (scriptProcessor.value) {
+      scriptProcessor.value.disconnect()
+    }
+    if (mediaStreamSource.value) {
+      mediaStreamSource.value.disconnect()
+    }
+    if (audioContext.value) {
+      audioContext.value.close()
+    }
+    
+    // 释放媒体流资源
+    if (mediaStream.value) {
+      mediaStream.value.getTracks().forEach(track => track.stop())
+      mediaStream.value = null
+    }
+    
+    // 停止轮询
+    stopResultPolling()
+    
+    // 通知后端停止识别
+    await stopBackendRecognition()
+  } else {
+    // 生成新的会话ID
+    sessionId.value = 'session_' + Date.now()
+    
+    // 记录输入框状态
+    recordInputState()
+    // 初始化倒计时前再次清除定时器(防止快速点击)
+    clearInterval(countdownTimer.value)
+    countdown.value = props.maxDuration // 设置最大录音时间
+
+    try {
+      // 启动后端识别服务
+      const startResult = await startBackendRecognition()
+      if (!startResult || startResult.status !== 'success') {
+        return
+      }
+
+      // 获取麦克风权限
+      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
+      mediaStream.value = stream
+
+      // 使用Web Audio API捕获音频并转换为PCM格式
+      audioContext.value = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 })
+      mediaStreamSource.value = audioContext.value.createMediaStreamSource(stream)
+      scriptProcessor.value = audioContext.value.createScriptProcessor(4096, 1, 1)
+
+      // 处理音频数据
+      scriptProcessor.value.onaudioprocess = async (event) => {
+        const inputData = event.inputBuffer.getChannelData(0)
+        // 转换为16位PCM
+        const pcmData = new Int16Array(inputData.length)
+        for (let i = 0; i < inputData.length; i++) {
+          pcmData[i] = inputData[i] * 32768
+        }
+        // 发送到后端
+        await sendAudioData(pcmData.buffer)
+      }
+
+      // 连接音频节点
+      mediaStreamSource.value.connect(scriptProcessor.value)
+      scriptProcessor.value.connect(audioContext.value.destination)
+
+      // 先设置UI状态,让用户知道系统正在准备
+      isRecording.value = true
+      emit('recordingStatusChanged', true)
+
+      // 启动轮询获取识别结果
+      startResultPolling()
+
+      // 启动倒计时
+      countdownTimer.value = setInterval(() => {
+        countdown.value--
+        if (countdown.value <= 0) {
+          clearInterval(countdownTimer.value) // 倒计时结束清除
+          // 停止录音
+          if (isRecording.value) {
+            isRecording.value = false
+            emit('recordingStatusChanged', false)
+            
+            // 停止音频处理
+            if (scriptProcessor.value) {
+              scriptProcessor.value.disconnect()
+            }
+            if (mediaStreamSource.value) {
+              mediaStreamSource.value.disconnect()
+            }
+            if (audioContext.value) {
+              audioContext.value.close()
+            }
+            
+            // 释放媒体流资源
+            if (mediaStream.value) {
+              mediaStream.value.getTracks().forEach(track => track.stop())
+              mediaStream.value = null
+            }
+            
+            // 停止轮询
+            stopResultPolling()
+            
+            // 通知后端停止识别
+            stopBackendRecognition()
+          }
+        }
+      }, 1000)
+    } catch (err) {
+      console.error('麦克风权限获取失败:', err)
+      if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
+        ElMessage.warning('麦克风权限被拒绝,请允许麦克风访问')
+      } else if (err.name === 'NotFoundError') {
+        ElMessage.warning('未找到麦克风设备')
+      } else if (err.name === 'NotReadableError') {
+        ElMessage.warning('麦克风被其他应用占用')
+      } else {
+        ElMessage.warning('启动录音失败,请检查浏览器权限设置')
+      }
+      // 出错时重置状态
+      isRecording.value = false
+      emit('recordingStatusChanged', false)
+      countdown.value = 0
+      
+      // 释放资源
+      if (mediaStream.value) {
+        mediaStream.value.getTracks().forEach(track => track.stop())
+        mediaStream.value = null
+      }
+    }
+  }
+}
+
+// 组件卸载时清理资源
+onUnmounted(() => {
+  clearInterval(countdownTimer.value)
+  stopResultPolling()
+  
+  // 停止音频处理
+  if (scriptProcessor.value) {
+    scriptProcessor.value.disconnect()
+  }
+  if (mediaStreamSource.value) {
+    mediaStreamSource.value.disconnect()
+  }
+  if (audioContext.value) {
+    audioContext.value.close()
+  }
+  
+  // 释放媒体流资源
+  if (mediaStream.value) {
+    mediaStream.value.getTracks().forEach(track => track.stop())
+    mediaStream.value = null
+  }
+  
+  // 通知后端停止识别
+  stopBackendRecognition()
+})
+</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>

+ 4 - 8
src/router/index.js

@@ -242,19 +242,17 @@ const router = createRouter({
 
 // 导航守卫
 router.beforeEach(async (to, from, next) => {
-
-  // 检查登录状态
-  const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
-  
   // 注册页面始终允许访问,不需要登录状态
   if (to.path === '/register-login') {
     next()
     return
   }
-  
+
+  // 检查登录状态
+  const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
   // 允许未登录用户访问的页面列表
   const allowedPages = Object.keys(loginToHomeMap)
-  
+
   // 如果未登录且不是允许访问的页面,重定向到登录页
   if (!isLoggedIn && !allowedPages.includes(to.path)) {
     next('/login')
@@ -277,8 +275,6 @@ router.beforeEach(async (to, from, next) => {
     return
   }
 
-  // 注册页面已经在前面处理过,这里不需要再处理
-
   // 获取当前登录类型
   const loginPath = localStorage.getItem('loginPath')
 

+ 5 - 0
src/views/AIPage/AIGeneralCourse.vue

@@ -119,6 +119,10 @@
       </div>
 
       <div class="box-2">
+
+        <SpeechRecognition />
+        <br/>
+
         <!-- 自主学习组件,只在AI自主学习时显示 -->
         <SelfDirectedLearning v-if="currentOpenedMenu === 'selfstudy'"
                               @refreshData="refreshData" />
@@ -168,6 +172,7 @@ import teachingImg from '@/assets/icon/teaching.png'
 import SelfDirectedLearning from '@/components/study/SelfDirectedLearning.vue'
 import DialogContent from "@/views/AIPage/aiGenerate/DialogContent.vue";
 import {teacherList} from "@/api/teachers.js";
+import SpeechRecognition from "@/views/AIPage/aiGenerate/SpeechRecognition.vue";
 
 const router = useRouter() // 获取当前路由对象
 // 下拉菜单选中项

+ 234 - 0
src/views/AIPage/aiGenerate/SpeechRecognition.vue

@@ -0,0 +1,234 @@
+<template>
+  <div class="speech-recognition">
+    <h2>实时语音识别</h2>
+
+    <div class="controls">
+      <button @click="startRecording" :disabled="isRecording">开始录音</button>
+      <button @click="stopRecording" :disabled="!isRecording">停止录音</button>
+    </div>
+
+    <div class="status">
+      <p>状态: {{ recordingStatus }}</p>
+    </div>
+
+    <div class="result">
+      <h3>识别结果</h3>
+      <div class="result-content">{{ recognitionResult }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import axios from '@/utils/request';
+
+const isRecording = ref(false);
+const recordingStatus = ref('就绪');
+const recognitionResult = ref('');
+const mediaRecorder = ref(null);
+const audioChunks = ref([]);
+const sessionId = ref('session_' + Date.now());
+let resultPollingInterval = null;
+
+// 开始录音
+const startRecording = async () => {
+  try {
+    // 检查浏览器兼容性
+    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+      recordingStatus.value = '当前浏览器不支持语音录制功能';
+      return;
+    }
+
+    // 启动后端识别服务
+    const startResult = await axios({
+      url: 'admin/speech/recognition/start?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: {}
+    });
+
+    if (startResult.status === 'success') {
+      // 获取麦克风权限
+      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+
+      // 使用Web Audio API捕获音频并转换为PCM格式
+      const audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
+      const mediaStreamSource = audioContext.createMediaStreamSource(stream);
+      const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
+
+      // 处理音频数据
+      scriptProcessor.onaudioprocess = async (event) => {
+        const inputData = event.inputBuffer.getChannelData(0);
+        // 转换为16位PCM
+        const pcmData = new Int16Array(inputData.length);
+        for (let i = 0; i < inputData.length; i++) {
+          pcmData[i] = inputData[i] * 32768;
+        }
+        // 发送到后端
+        await sendAudioData(pcmData.buffer);
+      };
+
+      // 连接音频节点
+      mediaStreamSource.connect(scriptProcessor);
+      scriptProcessor.connect(audioContext.destination);
+
+      // 存储音频上下文和处理器以便停止
+      mediaRecorder.value = {
+        stop: () => {
+          scriptProcessor.disconnect();
+          mediaStreamSource.disconnect();
+          audioContext.close();
+          stream.getTracks().forEach(track => track.stop());
+        },
+        state: 'recording'
+      };
+      isRecording.value = true;
+      recordingStatus.value = '录音中...';
+
+      // 启动轮询获取识别结果
+      startResultPolling();
+    } else {
+      recordingStatus.value = '启动识别服务失败';
+    }
+  } catch (error) {
+    console.error('启动录音失败:', error);
+    if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
+      recordingStatus.value = '麦克风权限被拒绝,请允许麦克风访问';
+    } else if (error.name === 'NotFoundError') {
+      recordingStatus.value = '未找到麦克风设备';
+    } else {
+      recordingStatus.value = '启动录音失败';
+    }
+  }
+};
+
+// 停止录音
+const stopRecording = async () => {
+  if (mediaRecorder.value && mediaRecorder.value.state === 'recording') {
+    mediaRecorder.value.stop();
+    isRecording.value = false;
+    recordingStatus.value = '录音已停止';
+
+    // 停止轮询
+    stopResultPolling();
+
+    // 通知后端停止识别
+    const stopResult = await axios({
+      url: 'admin/speech/recognition/stop?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: {}
+    });
+
+    if (stopResult.status !== 'success') {
+      recordingStatus.value = '停止识别服务失败';
+    }
+  }
+};
+
+// 启动轮询获取识别结果
+const startResultPolling = () => {
+  // 每300ms获取一次结果
+  resultPollingInterval = setInterval(async () => {
+    try {
+      const result = await axios({
+        url: 'admin/speech/recognition/result?sessionId=' + sessionId.value,
+        method: 'GET'
+      });
+
+      if (result.status === 'success' && result.data && result.data.result) {
+        recognitionResult.value = result.data.result;
+      }
+    } catch (error) {
+      console.error('获取识别结果失败:', error);
+    }
+  }, 300);
+};
+
+// 停止轮询
+const stopResultPolling = () => {
+  if (resultPollingInterval) {
+    clearInterval(resultPollingInterval);
+    resultPollingInterval = null;
+  }
+};
+
+// 发送音频数据到后端
+const sendAudioData = async (arrayBuffer) => {
+  try {
+    // 发送到后端
+    const result = await axios({
+      url: 'admin/speech/recognition/send?sessionId=' + sessionId.value,
+      method: 'POST',
+      data: arrayBuffer,
+      headers: {
+        'Content-Type': 'application/octet-stream'
+      }
+    });
+    if (result.status !== 'success') {
+      console.error('发送音频数据失败:', result.message);
+    }
+  } catch (error) {
+    console.error('处理音频数据失败:', error);
+  }
+};
+
+// 清理资源
+onUnmounted(() => {
+  if (mediaRecorder.value) {
+    mediaRecorder.value.stop();
+  }
+});
+</script>
+
+<style scoped>
+.speech-recognition {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #f5f5f5;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.controls {
+  margin: 20px 0;
+}
+
+button {
+  padding: 10px 20px;
+  margin-right: 10px;
+  border: none;
+  border-radius: 4px;
+  background: #4CAF50;
+  color: white;
+  cursor: pointer;
+  font-size: 16px;
+}
+
+button:disabled {
+  background: #cccccc;
+  cursor: not-allowed;
+}
+
+button:nth-child(2) {
+  background: #f44336;
+}
+
+.status {
+  margin: 20px 0;
+  padding: 10px;
+  background: #e3f2fd;
+  border-radius: 4px;
+}
+
+.result {
+  margin: 20px 0;
+}
+
+.result-content {
+  padding: 15px;
+  background: white;
+  border-radius: 4px;
+  min-height: 100px;
+  white-space: pre-wrap;
+}
+</style>

+ 41 - 36
src/views/RegisterLogin.vue

@@ -173,45 +173,50 @@ const rules = ref({
 
 // 处理注册
 const handleRegister = async () => {
-  try {
-    // 表单验证
-    await loginFormRef.value.validate();
-    // 获取租户名称
-    const tenantName = loginForm.value.tenantName;
 
-    if (tenantName === import.meta.env.VITE_APP_TITLE) {
-      ElMessage.error('此租户不支持注册!');
-      return;
-    }
+  if (!loginFormRef.value) return
+  await loginFormRef.value.validate(async valid => {
+    if (valid) {
+// 表单验证
+      try {
+        // 获取租户名称
+        const tenantName = loginForm.value.tenantName;
 
-    // 获取租户ID
-    const tenantId = await getTenantId(tenantName);
-    if (!tenantId) {
-      // 租户验证失败
-      return;
-    }
-    // 准备请求数据
-    const registerData = {
-      username: loginForm.value.phone,
-      nickname: loginForm.value.phone,
-      password: loginForm.value.password,
-      inviteCode: loginForm.value.inviteCode,
-      rememberMe: loginForm.value.rememberMe,
-      tenantId: tenantId
-    };
-    // 调用注册接口
-    const res = await registerSignUp({ 'Tenant-Id': tenantId },registerData);
-    // 注册成功处理
-    if (res && res.code === 0) {
-      ElMessage.success('注册成功,请登录');
-      goBackToLogin();
-    } else {
-      ElMessage.error(res && res.msg);
+        if (tenantName === import.meta.env.VITE_APP_TITLE) {
+          ElMessage.error('此租户不支持注册!');
+          return;
+        }
+
+        // 获取租户ID
+        const tenantId = await getTenantId(tenantName);
+        if (!tenantId) {
+          // 租户验证失败
+          return;
+        }
+        // 准备请求数据
+        const registerData = {
+          username: loginForm.value.phone,
+          nickname: loginForm.value.phone,
+          password: loginForm.value.password,
+          inviteCode: loginForm.value.inviteCode,
+          rememberMe: loginForm.value.rememberMe,
+          tenantId: tenantId
+        };
+        // 调用注册接口
+        const res = await registerSignUp({ 'Tenant-Id': tenantId },registerData);
+        // 注册成功处理
+        if (res && res.code === 0) {
+          ElMessage.success('注册成功,请登录');
+          goBackToLogin();
+        } else {
+          ElMessage.error(res && res.msg);
+        }
+      } catch (error) {
+        console.error('注册失败:', error);
+        ElMessage.error('注册失败,请检查输入信息或网络连接');
+      }
     }
-  } catch (error) {
-    console.error('注册失败:', error);
-    ElMessage.error('注册失败,请检查输入信息或网络连接');
-  }
+  })
 };
 
 </script>