丸子 il y a 8 mois
Parent
commit
2015f8c68f
3 fichiers modifiés avec 183 ajouts et 31 suppressions
  1. 168 13
      src/components/videopage/DialogComponents.vue
  2. 8 8
      src/views/AIDevelop.vue
  3. 7 10
      src/views/AIQuestions.vue

+ 168 - 13
src/components/videopage/DialogComponents.vue

@@ -105,14 +105,41 @@
             :questTip="currentQuestion.ccAiQuestTip || ''"
             @select-message="handleSelectMessage"
           />
+          <!-- 消息输入框 -->
           <el-input
             v-model="prompt"
             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 #append>
-              <el-button @click="handleSendByButton" size="large" round
+              <!-- 终止问答按钮 -->
+              <div
+                v-if="conversationInProgress"
+                @click="stopStream"
+                class="stop-btn"
+                title="终止问答"
+              >
+                <img :src="stopicon" alt="停止" />
+              </div>
+              <!-- 发送按钮 -->
+              <el-button v-if="!conversationInProgress" @click="handleSendByButton" size="large" round
                 >发送</el-button
               >
             </template>
@@ -132,6 +159,12 @@ 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 stopicon from '@/assets/icon/stopicon.png'
+
 // 导入图标
 import auto from '@/assets/icon/auto_awesome.png'
 
@@ -154,7 +187,7 @@ const messageList = ref([])
 const prompt = ref('')
 const messageContainer = ref(null)
 const aiQuestionCount = ref(0)
-const userScrolled = ref(false)//是否用户手动滚动
+const userScrolled = ref(false) //是否用户手动滚动
 const xZAiData = ref({})
 const activeConversationId = ref(null)
 const conversationInProgress = ref(false)
@@ -164,10 +197,16 @@ const isComposing = ref(false)
 const inputTimeout = ref()
 const enableContext = ref(true)
 
-//tts
+// tts
 import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
 const { playAudioChunk } = useAudioPlayer();
 
+// 语音输入响应式变量
+const isRecording = ref(false) // 录音状态
+const recognition = ref(null) // 语音识别实例
+const countdown = ref(0) // 倒计时剩余秒数
+const countdownTimer = ref(null) // 倒计时定时器
+
 // 处理选择的默认消息
 const handleSelectMessage = message => {
   prompt.value = message
@@ -195,7 +234,7 @@ const handleAIClick = async () => {
   messageList.value = []
   showAIDialog.value = true
 
-  //创建对话
+  // 创建对话
   await createAiChart()
 
   if (props.currentQuestion.ccQuestContent) {
@@ -209,7 +248,6 @@ const handleAIClick = async () => {
       content: props.currentQuestion.ccQuestContent,
       contentAnswer: props.currentQuestion.ccAiAnswer,
     })
-
   }
 }
 
@@ -243,7 +281,6 @@ const getXzAi = async () => {
 const createAiChart = async () => {
   // 先获取数字人接口
   await getXzAi()
-
   // 智能问答
   await CreateDialogue({ roleId: xZAiData.value.id })
     .then(res => {
@@ -254,7 +291,7 @@ const createAiChart = async () => {
       console.error('请求出错:', error)
     })
 }
-
+ 
 // 发送消息
 const sendMessage = async () => {
   if (prompt.value.trim()) {
@@ -266,7 +303,7 @@ const sendMessage = async () => {
 
     // 增加问答次数
     aiQuestionCount.value++
-
+    
     // 保存AI问答次数
     try {
       await saveRecord({
@@ -290,6 +327,89 @@ const sendMessage = async () => {
   }
 }
 
+
+// =========== 【语音录入】相关 ===========
+// 初始化语音识别
+const initSpeechRecognition = () => {
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
+  if (!SpeechRecognition) {
+    ElMessage.warning('当前浏览器不支持语音输入功能')
+    return null
+  }
+
+  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
+    }
+  }
+
+  // 新增:识别器结束时清除定时器
+  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 回复
 const simulateAIResponse = question => {
   return new Promise(resolve => {
@@ -890,12 +1010,24 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
   }
 }
 
+// 终止按钮
+.stop-btn {
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  padding: rpx(5);
+  img {
+    width: rpx(20);
+    height: rpx(20);
+  }
+}
+
 // 用户输入框样式
 .user-input {
+  gap: rpx(5); // 间距
   ::v-deep(.el-input__wrapper) {
     height: rpx(23);
-    border-top-left-radius: rpx(5);
-    border-bottom-left-radius: rpx(5);
+    border-radius: rpx(5);
     border-color: rgba($primary-color, 0.3);
 
     &:focus-within {
@@ -907,12 +1039,33 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
     font-size: rpx(10);
     text-indent: 1em;
   }
-  ::v-deep(.el-input-group__append, .el-input-group__prepend) {
+  
+  // 语音按钮样式
+  ::v-deep(.el-input-group__prepend) {
+    width: rpx(15);
+    background: white;
+    border-radius: rpx(5);
+    text-align: center;
+  }
+  
+  ::v-deep(.el-input-group__prepend .el-button.recording) {
+    padding: rpx(5) rpx(10);
+    background: #fff;
+    border: none;
+    border-radius: rpx(5);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    color: #dc3545;
+  }
+  
+  ::v-deep(.el-input-group__append) {
+    border: none;
     background: linear-gradient(to bottom, #ab81ff, #8559dc);
-    border-top-right-radius: rpx(5);
-    border-bottom-right-radius: rpx(5);
+    border-radius: rpx(5);
     color: white;
     font-size: rpx(9);
+    border-left: none;
   }
 }
 
@@ -956,4 +1109,6 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
   margin-top: rpx(-10);
   margin-bottom: rpx(5);
 }
+
+
 </style>

+ 8 - 8
src/views/AIDevelop.vue

@@ -450,14 +450,14 @@ onMounted(async () => {
         }
 
         // 手动修改第一个课程为image类型用于测试
-        // if (index === 0) {
-        //   courseTemp.courseContentType = 'ppt';
-        //   courseTemp.pptPath = 'http://59.110.91.129:8088/admin-api/infra/file/29/get/20250820/ppt_1755654972861.pptx';
-        //   // courseTemp.courseContentType = 'image';
-        //   // courseTemp.courseImagePath = 'http://59.110.91.129:8088/admin-api/infra/file/4/get/20250715/one_1752549934393.png,http://59.110.91.129:8088/admin-api/infra/file/29/get/20250722/666_1753151547130.png';
-        //   // // 可选:修改课程名称以便识别
-        //   courseTemp.courseName = '测试';
-        // }
+        if (index === 0) {
+          // courseTemp.courseContentType = 'ppt';
+          // courseTemp.pptPath = 'http://59.110.91.129:8088/admin-api/infra/file/29/get/20250820/ppt_1755654972861.pptx';
+          courseTemp.courseContentType = 'image';
+          courseTemp.courseImagePath = 'http://59.110.91.129:8088/admin-api/infra/file/4/get/20250715/one_1752549934393.png,http://59.110.91.129:8088/admin-api/infra/file/29/get/20250722/666_1753151547130.png';
+          // 可选:修改课程名称以便识别
+          courseTemp.courseName = '测试';
+        }
 
         if (topName === courseTemp.courseLabel) {
           let topMenu = menuItems.value[menuItems.value.length - 1]

+ 7 - 10
src/views/AIQuestions.vue

@@ -72,16 +72,14 @@
               />
               <!-- 添加语音输入按钮 -->
               <button
-                @click="toggleSpeechInput"
-                class="speech-btn"
-                :class="{ recording: isRecording }"
+                  @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
-                >
+                <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
               </button>
 
               <!-- 终止问答按钮 -->
@@ -237,8 +235,7 @@ const getConversation = async (id) => {
 // =========== 【语音录入】相关 ===========
 // 初始化语音识别
 const initSpeechRecognition = () => {
-  const SpeechRecognition =
-    window.SpeechRecognition || window.webkitSpeechRecognition;
+  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
   if (!SpeechRecognition) {
     alert("当前浏览器不支持语音输入功能");
     return null;
@@ -272,6 +269,7 @@ const initSpeechRecognition = () => {
   return instance;
 };
 
+
 // 切换录音状态
 const toggleSpeechInput = () => {
   // 无论当前状态如何,先清除可能存在的旧定时器
@@ -291,8 +289,7 @@ const toggleSpeechInput = () => {
     recognition.value = initSpeechRecognition();
     if (!recognition.value) return;
 
-    navigator.mediaDevices
-      .getUserMedia({ audio: true })
+    navigator.mediaDevices.getUserMedia({ audio: true })
       .then(() => {
         recognition.value.start();
         isRecording.value = true;