Ver código fonte

同步优化调用语音输入组件的绘画、视频、问题等

liyanbo 3 meses atrás
pai
commit
1124d01d4e

+ 61 - 14
src/components/ai/image/ImageToImage.vue

@@ -81,13 +81,13 @@
         <!-- 输入框和发送按钮 -->
         <div class="input-section">
           <input
-            ref="inputRef"
-            type="text"
-            v-model="inputMessage"
-            placeholder="描述任何画面..."
-            @keyup.enter="sendMessage"
-            style="flex: 1; margin-right: 8px;"
-          />
+              ref="inputRef"
+              type="text"
+              v-model="displayedPrompt"
+              placeholder="描述任何画面..."
+              @keyup.enter="sendMessage"
+              style="flex: 1; margin-right: 8px;"
+            />
           <!-- 风格选择下拉框 -->
           <el-select 
             v-model="selectedStyle" 
@@ -111,6 +111,7 @@
           <!-- 语音输入按钮 -->
           <VoiceInput
               @voiceRecognized="handleVoiceRecognized"
+              @recordingStatusChanged="handleRecordingStatusChanged"
               lang="zh-CN"
               maxDuration="10"
           />
@@ -133,7 +134,7 @@
 </template>
 
 <script setup>
-import {ref, onMounted, onUnmounted, defineEmits} from 'vue'
+import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
 import {AiImageStatusEnum, CreatePainting, PaintingGetMys} from '@/api/questions.js'
 import VoiceInput from '../voice/VoiceInput.vue'
 
@@ -225,10 +226,57 @@ const inputRef = ref(null)
 
 
 
-// 处理语音识别结果
-const handleVoiceRecognized = (transcript) => {
-  inputMessage.value += transcript;
-}
+// 导入计算属性
+
+// 语音输入状态跟踪
+const isVoiceRecording = ref(false); // 当前是否正在录音
+const voiceRecognizedText = ref(""); // 实时语音识别结果
+
+// 用于控制输入框显示的内容
+const displayedPrompt = computed({
+  get() {
+    // 录音时,显示inputMessage.value + 实时语音识别结果
+    if (isVoiceRecording.value) {
+      return inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+    }
+    // 不录音时,只显示inputMessage.value
+    return inputMessage.value;
+  },
+  set(newValue) {
+    // 只在用户手动输入时更新inputMessage.value
+    if (!isVoiceRecording.value) {
+      inputMessage.value = newValue;
+    }
+  }
+});
+
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  if (isVoiceRecording.value) {
+    // 在同一次录音过程中,只更新临时变量,不修改inputMessage.value
+    voiceRecognizedText.value = text;
+  } else {
+    // 在录音结束时,将最终的语音内容追加到inputMessage.value
+    inputMessage.value = inputMessage.value ? `${inputMessage.value} ${text}` : text;
+    // 清空临时变量
+    voiceRecognizedText.value = "";
+  }
+};
+
+// 处理录音状态变化
+const handleRecordingStatusChanged = (status) => {
+  const wasRecording = isVoiceRecording.value;
+  isVoiceRecording.value = status;
+
+  // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到inputMessage.value
+  if (wasRecording && !isVoiceRecording.value) {
+    if (voiceRecognizedText.value) {
+      inputMessage.value = inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+      // 清空临时变量
+      voiceRecognizedText.value = "";
+    }
+  }
+};
 
 // 停止操作函数
 const stopStream = async () => {
@@ -686,5 +734,4 @@ const download = (event, activeIndex) => {
      border: 1px solid #ffce1b;
     border-radius: rpx(5);
   }
-</style>
-
+</style>

+ 57 - 11
src/components/ai/image/TextToImage.vue

@@ -106,14 +106,15 @@
         <!-- 输入框和发送按钮 -->
         <div class="input-section">
           <input
-            type="text"
-            v-model="inputMessage"
-            placeholder="描述任何画面..."
-            @keyup.enter="sendMessage"
-          />
+              type="text"
+              v-model="displayedPrompt"
+              placeholder="描述任何画面..."
+              @keyup.enter="sendMessage"
+            />
           <!-- 语音输入按钮 -->
           <VoiceInput
               @voiceRecognized="handleVoiceRecognized"
+              @recordingStatusChanged="handleRecordingStatusChanged"
               lang="zh-CN"
               maxDuration="10"
           />
@@ -135,7 +136,7 @@
 </template>
 
 <script setup>
-import {ref, onMounted, onUnmounted, defineEmits} from 'vue'
+import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
 import {AiImageStatusEnum, CreatePainting, PaintingGetMys} from '@/api/questions.js'
 import demo1 from '@/assets/images/ai-demo/ai-image-demo1.png'
 import demo2 from '@/assets/images/ai-demo/ai-image-demo2.png'
@@ -209,10 +210,55 @@ onMounted(async () => {
 const messages = ref([])
 const inputMessage = ref('')
 
-// 处理语音识别结果
-const handleVoiceRecognized = (transcript) => {
-  inputMessage.value += transcript;
-}
+// 语音输入状态跟踪
+const isVoiceRecording = ref(false); // 当前是否正在录音
+const voiceRecognizedText = ref(""); // 实时语音识别结果
+
+// 用于控制输入框显示的内容
+const displayedPrompt = computed({
+  get() {
+    // 录音时,显示inputMessage.value + 实时语音识别结果
+    if (isVoiceRecording.value) {
+      return inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+    }
+    // 不录音时,只显示inputMessage.value
+    return inputMessage.value;
+  },
+  set(newValue) {
+    // 只在用户手动输入时更新inputMessage.value
+    if (!isVoiceRecording.value) {
+      inputMessage.value = newValue;
+    }
+  }
+});
+
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  if (isVoiceRecording.value) {
+    // 在同一次录音过程中,只更新临时变量,不修改inputMessage.value
+    voiceRecognizedText.value = text;
+  } else {
+    // 在录音结束时,将最终的语音内容追加到inputMessage.value
+    inputMessage.value = inputMessage.value ? `${inputMessage.value} ${text}` : text;
+    // 清空临时变量
+    voiceRecognizedText.value = "";
+  }
+};
+
+// 处理录音状态变化
+const handleRecordingStatusChanged = (status) => {
+  const wasRecording = isVoiceRecording.value;
+  isVoiceRecording.value = status;
+
+  // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到inputMessage.value
+  if (wasRecording && !isVoiceRecording.value) {
+    if (voiceRecognizedText.value) {
+      inputMessage.value = inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+      // 清空临时变量
+      voiceRecognizedText.value = "";
+    }
+  }
+};
 
 // 停止操作函数
 const stopStream = async () => {
@@ -575,4 +621,4 @@ const download = (activeIndex) => {
     box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
 
 }
-</style>
+</style>

+ 50 - 4
src/components/ai/video/ImageToVideo.vue

@@ -67,7 +67,7 @@
           <div class="input-section">
             <input
               type="text"
-              v-model="inputMessage"
+              v-model="displayedPrompt"
               placeholder="描述任何画面..."
               @keyup.enter="sendMessage"
               style="flex: 1; margin-right: 8px;"
@@ -76,6 +76,7 @@
             <!-- 语音输入按钮 -->
                 <VoiceInput
                     @voiceRecognized="handleVoiceRecognized"
+                    @recordingStatusChanged="handleRecordingStatusChanged"
                     lang="zh-CN"
                     maxDuration="10"
                 />
@@ -98,7 +99,7 @@
 </template>
 
 <script setup>
-import {ref, onMounted, onUnmounted, defineEmits} from 'vue'
+import {ref, onMounted, onUnmounted, defineEmits, computed} from 'vue'
 import {AiImageStatusEnum, CreateVideo, VideoGetMys} from '@/api/questions.js'
 import { useRouter } from 'vue-router'
 
@@ -175,9 +176,54 @@ onMounted(async () => {
 const messages = ref([])
 const inputMessage = ref('')
 
+// 语音输入状态跟踪
+const isVoiceRecording = ref(false); // 当前是否正在录音
+const voiceRecognizedText = ref(""); // 实时语音识别结果
+
+// 用于控制输入框显示的内容
+const displayedPrompt = computed({
+  get() {
+    // 录音时,显示inputMessage.value + 实时语音识别结果
+    if (isVoiceRecording.value) {
+      return inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+    }
+    // 不录音时,只显示inputMessage.value
+    return inputMessage.value;
+  },
+  set(newValue) {
+    // 只在用户手动输入时更新inputMessage.value
+    if (!isVoiceRecording.value) {
+      inputMessage.value = newValue;
+    }
+  }
+});
+
 // 语音输入识别结果处理
 const handleVoiceRecognized = (text) => {
-  inputMessage.value += text;
+  if (isVoiceRecording.value) {
+    // 在同一次录音过程中,只更新临时变量,不修改inputMessage.value
+    voiceRecognizedText.value = text;
+  } else {
+    // 在录音结束时,将最终的语音内容追加到inputMessage.value
+    inputMessage.value = inputMessage.value ? `${inputMessage.value} ${text}` : text;
+    // 清空临时变量
+    voiceRecognizedText.value = "";
+  }
+};
+
+// 处理录音状态变化
+const handleRecordingStatusChanged = (status) => {
+  const wasRecording = isVoiceRecording.value;
+  isVoiceRecording.value = status;
+
+  // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到inputMessage.value
+  if (wasRecording && !isVoiceRecording.value) {
+    if (voiceRecognizedText.value) {
+      inputMessage.value = inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+      // 清空临时变量
+      voiceRecognizedText.value = "";
+    }
+  }
 };
 
 // 停止操作函数
@@ -549,4 +595,4 @@ const inProgressTimerFun = () => {
   justify-content: center;
   align-items: center;
 }
-</style>
+</style>

+ 70 - 103
src/components/videopage/DialogComponents.vue

@@ -107,26 +107,21 @@
           />
           <!-- 消息输入框 -->
           <el-input
-            v-model="prompt"
+            v-model="displayedPrompt"
             placeholder="输入问题..."
             class="user-input"
             @keyup.enter="handleSendByKeydown"
           >
           <!-- 语音输入 -->
-            <template #prepend>
-              <el-button 
-                @click="toggleSpeechInput"
-                size="small" 
-                :class="{ 'recording': isRecording }"
-                circle
-              >
-                <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>
-              </el-button>
-            </template>
-            
+              <template #prepend>
+               <VoiceInput
+                 @voiceRecognized="handleVoiceRecognized"
+                 @recordingStatusChanged="handleRecordingStatusChanged"
+                 lang="zh-CN"
+                 maxDuration="10"
+               />
+              </template>
+
             <!-- 终止按钮和发送按钮条件渲染 -->
             <template #append>
               <!-- 终止问答按钮 -->
@@ -151,16 +146,14 @@
 </template>
 
 <script setup>
-import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
+import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick, computed} from 'vue'
 import { ElMessage } from 'element-plus'
 import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
 import { teacherList } from '@/api/teachers.js'
 import DefaultMessage from '@/components/DefaultMessage/index.vue'
 import MarkdownView from '@/components/MarkdownView/index.vue'
 import { saveRecord } from '@/api/personalized/index.js'
-
-// 语音图标导入
-import { Microphone, Mute } from '@element-plus/icons-vue'
+import VoiceInput from '../ai/voice/VoiceInput.vue'
 
 // 终止
 import stopicon from '@/assets/icon/stopicon.png'
@@ -201,11 +194,27 @@ const enableContext = ref(true)
 import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
 const { playAudioChunk , stopPlayback  } = useAudioPlayer();
 
-// 语音输入响应式变量
-const isRecording = ref(false) // 录音状态
-const recognition = ref(null) // 语音识别实例
-const countdown = ref(0) // 倒计时剩余秒数
-const countdownTimer = ref(null) // 倒计时定时器
+// 语音输入状态跟踪
+const isVoiceRecording = ref(false); // 当前是否正在录音
+const voiceRecognizedText = ref(""); // 实时语音识别结果
+
+// 用于控制输入框显示的内容
+const displayedPrompt = computed({
+  get() {
+    // 录音时,显示prompt.value + 实时语音识别结果
+    if (isVoiceRecording.value) {
+      return prompt.value ? `${prompt.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+    }
+    // 不录音时,只显示prompt.value
+    return prompt.value;
+  },
+  set(newValue) {
+    // 只在用户手动输入时更新prompt.value
+    if (!isVoiceRecording.value) {
+      prompt.value = newValue;
+    }
+  }
+});
 
 // 处理选择的默认消息
 const handleSelectMessage = message => {
@@ -299,7 +308,7 @@ const createAiChart = async () => {
       console.error('请求出错:', error)
     })
 }
- 
+
 // 发送消息
 const sendMessage = async () => {
   if (prompt.value.trim()) {
@@ -322,86 +331,33 @@ const sendMessage = async () => {
 }
 
 
-// =========== 【语音录入】相关 ===========
-// 初始化语音识别
-const initSpeechRecognition = () => {
-  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
-  if (!SpeechRecognition) {
-    ElMessage.warning('当前浏览器不支持语音输入功能')
-    return null
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  if (isVoiceRecording.value) {
+    // 在同一次录音过程中,只更新临时变量,不修改prompt.value
+    voiceRecognizedText.value = text;
+  } else {
+    // 在录音结束时,将最终的语音内容追加到prompt.value
+    prompt.value = prompt.value ? `${prompt.value} ${text}` : text;
+    // 清空临时变量
+    voiceRecognizedText.value = "";
   }
-
-  const instance = new SpeechRecognition()
-  instance.lang = "zh-CN"
-  instance.interimResults = false
-
-  instance.onresult = (event) => {
-    if (event.results?.[0]?.[0]) {
-      prompt.value += event.results[0][0].transcript
+};
+
+// 处理录音状态变化
+const handleRecordingStatusChanged = (isRecording) => {
+  const wasRecording = isVoiceRecording.value;
+  isVoiceRecording.value = isRecording;
+
+  // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到prompt.value
+  if (wasRecording && !isRecording) {
+    if (voiceRecognizedText.value) {
+      prompt.value = prompt.value ? `${prompt.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+      // 清空临时变量
+      voiceRecognizedText.value = "";
     }
   }
-
-  // 识别器结束时清除定时器
-  instance.onend = () => {
-    clearInterval(countdownTimer.value)
-    isRecording.value = false
-    countdown.value = 0
-  }
-
-  instance.onerror = (event) => {
-    console.error("语音识别错误:", event.error)
-    clearInterval(countdownTimer.value) // 出错时清除定时器
-    isRecording.value = false
-    ElMessage.error('语音输入失败,请重试')
-    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
-  } else {
-    // 初始化倒计时前再次清除定时器(防止快速点击)
-    clearInterval(countdownTimer.value)
-    countdown.value = 10 // 重置为10秒
-
-    recognition.value = initSpeechRecognition()
-    if (!recognition.value) return
-
-    navigator.mediaDevices.getUserMedia({ audio: true })
-      .then(() => {
-        recognition.value.start()
-        isRecording.value = true
-
-        // 启动新的倒计时定时器
-        countdownTimer.value = setInterval(() => {
-          countdown.value--
-          if (countdown.value <= 0) {
-            clearInterval(countdownTimer.value) // 倒计时结束清除
-            recognition.value.stop()
-            isRecording.value = false
-            countdown.value = 0
-          }
-        }, 1000)
-      })
-      .catch((err) => {
-        console.error("麦克风权限获取失败:", err)
-        ElMessage.warning('请允许麦克风权限以使用语音输入')
-        // 出错时重置状态
-        isRecording.value = false
-        countdown.value = 0
-      })
-  }
-}
+};
 
 
 // 模拟 AI 回复
@@ -1096,10 +1052,21 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
   
   // 语音按钮样式
   ::v-deep(.el-input-group__prepend) {
-    width: rpx(15);
+    width: rpx(25); // 非录音状态的默认宽度
+    min-width: rpx(25);
     background: white;
     border-radius: rpx(5);
     text-align: center;
+    padding: 0;
+    transition: width 0.3s ease;
+  }
+  
+  // 录音状态下的样式
+  ::v-deep(.el-input-group__prepend) {
+    &.recording {
+      width: rpx(60); // 录音状态下增加宽度
+      min-width: rpx(60);
+    }
   }
   
   ::v-deep(.el-input-group__prepend .el-button.recording) {