Jelajahi Sumber

AI对话课:
1、调整语音组件迁移到用户输入组件内,不在操控按钮组件内
2、调整操控组件按钮样式间距
3、单选提问没有答案的话,提示此题没有标准答案并给予鼓励

liyanbo 2 bulan lalu
induk
melakukan
0f5ce2b9f9

+ 2 - 46
src/components/aiCourse/DialogEngine.vue

@@ -39,13 +39,9 @@
       <!-- 控制按钮 -->
       <InputButtons
         :can-prev="!isAtFirstDialogue"
-        :can-next="!isAtLastDialogue && !isUserInputWaiting"
-        :show-voice="showVoiceInput"
-        :is-recording="isVoiceRecording"
+        :can-next="!isAtLastDialogue"
         @prev="playPrevious"
         @next="playNext"
-        @voice-recognized="handleVoiceRecognized"
-        @recording-status-changed="handleRecordingStatusChanged"
       />
     </div>
   </div>
@@ -114,10 +110,6 @@ const isPlaying = ref(false);             // 是否正在播放
 const showMask = ref(true);               // 是否显示遮罩层
 const isPlaybackStarted = ref(false);     // 播放是否已开始
 
-// 语音输入状态
-const isVoiceRecording = ref(false);      // 是否正在录音
-const voiceRecognizedText = ref("");      // 语音识别结果
-
 // 诗词显示状态
 const showPoem = ref(false);              // 是否显示诗词
 const currentPoemContent = ref('');       // 当前诗词内容
@@ -168,16 +160,6 @@ const previousQuestDialogue = computed(() => {
   return null;
 });
 
-// 是否显示语音输入
-const showVoiceInput = computed(() => {
-  if (currentDialogue.value?.type !== 'user') return false;
-  const prev = previousQuestDialogue.value;
-  return prev?.questionType !== 'singleChoice'; // 非单选问题时显示语音输入
-});
-
-// 是否等待用户输入
-const isUserInputWaiting = computed(() => currentDialogue.value?.type === 'user');
-
 // 是否在第一个对话
 const isAtFirstDialogue = computed(() => {
   return currentSectionIndex.value === 0 && currentDialogueIndex.value === 0;
@@ -208,32 +190,6 @@ const getPreviousDialogue = () => {
   return null;
 };
 
-/**
- * 处理语音识别结果
- * @param {Object} data - 语音识别数据
- */
-const handleVoiceRecognized = (data) => {
-  console.log('语音识别结果:', data.originalText);
-  if (isVoiceRecording.value) {
-    voiceRecognizedText.value = data.originalText;
-  } else {
-    voiceRecognizedText.value = "";
-  }
-};
-
-/**
- * 处理录音状态变化
- * @param {Boolean} isRecording - 是否正在录音
- */
-const handleRecordingStatusChanged = (isRecording) => {
-  console.log('录音状态:', isRecording);
-  const wasRecording = isVoiceRecording.value;
-  isVoiceRecording.value = isRecording;
-  if (wasRecording && !isRecording) {
-    voiceRecognizedText.value = "";
-  }
-};
-
 /**
  * 停止所有音频播放
  */
@@ -652,7 +608,7 @@ const handleSingleChoiceSubmit = async (data) => {
 
   const optionLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
   const optionsStr = dialogue.options.map((opt, idx) => `${optionLabels[idx]}. ${opt.content}`).join(';');
-  const content = `问题:${dialogue.content}\n选项:${optionsStr}\n我的答案:${data.label}\n正确答案:${dialogue.answer}\n\n请判断我的答案是否正确,并给予鼓励或夸赞,回复请精简,控制在50字内。`;
+  const content = `问题:${dialogue.content}\n选项:${optionsStr}\n我的答案:${data.label}\n正确答案:${dialogue.answer}\n\n请判断我的答案是否正确(如果没有正确答案则提示此题没有标准答案),并给予鼓励或夸赞,回复请精简,控制在50字内。`;
 
   console.log('发送单选问题:', content);
 

+ 173 - 2
src/components/aiCourse/dialog/dialogType/questType/UserInputCard.vue

@@ -11,6 +11,16 @@
         @keyup.enter.exact="handleSubmit"
       ></textarea>
       <div class="input-actions">
+        <!-- 语音输入按钮 -->
+        <div class="voice-input-outer" :class="{ 'recording': isRecording }">
+          <VoiceInput
+            inputSelector=".user-input-textarea"
+            lang="zh-CN"
+            maxDuration="10"
+            @voiceRecognized="handleVoiceRecognized"
+            @recordingStatusChanged="handleRecordingStatusChanged"
+          />
+        </div>
         <button class="cancel-btn" @click="handleCancel">清空</button>
         <button class="submit-btn" @click="handleSubmit">发送</button>
       </div>
@@ -20,6 +30,7 @@
 
 <script setup>
 import { ref } from 'vue';
+import VoiceInput from '@/components/ai/voice/VoiceInput_Api.vue';
 
 /**
  * UserInputCard - 用户文本输入卡片组件
@@ -29,10 +40,13 @@ import { ref } from 'vue';
 /**
  * Emits 定义
  * @event submit - 用户提交输入内容
+ * @event voice-recognized - 语音识别完成
+ * @event recording-status-changed - 录音状态变化
  */
-const emit = defineEmits(['submit']);
+const emit = defineEmits(['submit', 'voice-recognized', 'recording-status-changed']);
 
 const userInput = ref('');
+const isRecording = ref(false);
 
 /**
  * 处理提交
@@ -53,6 +67,30 @@ const handleSubmit = () => {
 const handleCancel = () => {
   userInput.value = '';
 };
+
+/**
+ * 处理语音识别完成
+ * @param {Object} result - 语音识别结果对象
+ * @param {string} result.originalText - 原始识别文本
+ * @param {string} result.processedText - 处理后的文本(包含原有内容)
+ * @param {number} result.cursorPos - 光标位置
+ */
+const handleVoiceRecognized = (result) => {
+  if (result && typeof result === 'object') {
+    userInput.value = result.processedText || result.originalText || '';
+  } else {
+    userInput.value = String(result || '');
+  }
+  emit('voice-recognized', result);
+};
+
+/**
+ * 处理录音状态变化
+ */
+const handleRecordingStatusChanged = (status) => {
+  isRecording.value = status === 'recording';
+  emit('recording-status-changed', status);
+};
 </script>
 
 <style scoped lang="scss">
@@ -161,12 +199,145 @@ const handleCancel = () => {
 
 .input-actions {
   display: flex;
+  align-items: center;
   justify-content: flex-end;
-  gap: rpx(6);
+  gap: rpx(8);
   margin-top: rpx(6);
   width: 100%;
 }
 
+.voice-input-outer {
+  position: relative;
+  width: rpx(28);
+  height: rpx(28);
+  border-radius: 50%;
+  background: transparent;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &::before {
+    content: '';
+    position: absolute;
+    width: 85%;
+    height: 85%;
+    border-radius: 50%;
+    border: rpx(1.5) solid rgba(80, 190, 240, 0.6);
+  }
+
+  &.recording {
+    &::before {
+      animation: pulse 2s infinite;
+      border-color: rgba(0, 100, 192, 0.6);
+    }
+
+    &::after {
+      content: '';
+      position: absolute;
+      width: 85%;
+      height: 85%;
+      border-radius: 50%;
+      border: rpx(1.5) solid rgba(0, 100, 192, 0.4);
+      animation: pulse 2s infinite 0.5s;
+    }
+
+    :deep(.voice-input-container) {
+      .speech-btn {
+        background: linear-gradient(135deg, #A0DCF0, #50BEF0);
+        border-color: rgba(0, 100, 192, 1);
+
+        .el-icon {
+          color: #0064BE;
+        }
+      }
+    }
+  }
+
+  :deep(.voice-input-container) {
+    position: relative;
+    z-index: 10;
+
+    .speech-btn {
+      width: rpx(22);
+      height: rpx(22);
+      border-radius: 50%;
+      border: rpx(1.5) solid rgba(0, 100, 192);
+      background: linear-gradient(135deg, #A0DCF0, #50BEF0);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 0;
+      gap: 0;
+      transition: all 0.3s ease;
+      cursor: pointer;
+      position: relative;
+      overflow: hidden;
+
+      &:hover {
+        transform: scale(1.05);
+        box-shadow: 0 rpx(3) rpx(8) rgba(0, 0, 0, 0.3);
+      }
+
+      &:active {
+        transform: scale(0.95);
+      }
+
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        width: 0;
+        height: 0;
+        border-radius: 50%;
+        background: rgba(255, 255, 255, 0.5);
+        transform: translate(-50%, -50%);
+        transition: width 0.6s, height 0.6s;
+      }
+
+      &:active::before {
+        width: rpx(50);
+        height: rpx(50);
+      }
+
+      .el-icon {
+        font-size: rpx(12);
+        color: #0064BE;
+        z-index: 1;
+      }
+
+      .countdown-text {
+        display: block;
+        font-size: rpx(6);
+        color: #0064BE;
+        position: absolute;
+        bottom: rpx(3);
+        left: 50%;
+        transform: translateX(-50%);
+      }
+
+      .waveform-container {
+        .live-waveform {
+          .bar {
+            background: rgba(0, 100, 190, 0.5) !important;
+          }
+        }
+      }
+    }
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+    opacity: 1;
+  }
+  100% {
+    transform: scale(1.15);
+    opacity: 0;
+  }
+}
+
 .cancel-btn, .submit-btn {
   padding: rpx(2.5) rpx(10);
   border: none;

+ 4 - 156
src/components/aiCourse/engine/InputButtons.vue

@@ -4,18 +4,6 @@
     <div class="arrow-icon-circle" @click="$emit('prev')" :class="{ 'disabled': !canPrev }">
       <el-icon class="arrow-icon"><CaretLeft /></el-icon>
     </div>
-    <!-- 语音输入按钮 -->
-    <div class="voice-input-outer" v-if="showVoice" :class="{ 'recording': isRecording }">
-      <VoiceInput
-        inputSelector=".user-input-textarea"
-        lang="zh-CN"
-        maxDuration="10"
-        @voiceRecognized="$emit('voice-recognized', $event)"
-        @recordingStatusChanged="$emit('recording-status-changed', $event)"
-      />
-    </div>
-    <!-- 语音输入按钮占位符 -->
-    <div class="voice-input-outer placeholder" v-else></div>
     <!-- 下一个对话按钮 -->
     <div class="arrow-icon-circle" @click="$emit('next')" :class="{ 'disabled': !canNext }">
       <el-icon class="arrow-icon"><CaretRight /></el-icon>
@@ -25,19 +13,16 @@
 
 <script setup>
 import { CaretLeft, CaretRight } from '@element-plus/icons-vue';
-import VoiceInput from '@/components/ai/voice/VoiceInput_Api.vue';
 
 /**
  * InputButtons - 底部控制按钮组件
- * 包含上一句/下一句切换按钮和语音输入按钮
+ * 包含上一句/下一句切换按钮
  */
 
 /**
  * Props 定义
  * @prop {Boolean} canPrev - 是否可以切换到上一句
  * @prop {Boolean} canNext - 是否可以切换到下一句
- * @prop {Boolean} showVoice - 是否显示语音输入按钮
- * @prop {Boolean} isRecording - 是否正在录音
  */
 defineProps({
   canPrev: {
@@ -47,14 +32,6 @@ defineProps({
   canNext: {
     type: Boolean,
     default: true
-  },
-  showVoice: {
-    type: Boolean,
-    default: false
-  },
-  isRecording: {
-    type: Boolean,
-    default: false
   }
 });
 
@@ -62,10 +39,8 @@ defineProps({
  * Emits 定义
  * @event prev - 切换到上一句
  * @event next - 切换到下一句
- * @event voice-recognized - 语音识别完成
- * @event recording-status-changed - 录音状态变化
  */
-defineEmits(['prev', 'next', 'voice-recognized', 'recording-status-changed']);
+defineEmits(['prev', 'next']);
 </script>
 
 <style scoped lang="scss">
@@ -86,9 +61,9 @@ defineEmits(['prev', 'next', 'voice-recognized', 'recording-status-changed']);
   width: 100%;
   z-index: 10;
   transition: all 0.3s ease;
-  gap: rpx(10);
+  gap: rpx(20);
   margin-bottom: 0;
-  padding-bottom: 0;
+  padding-bottom: rpx(10);
 }
 
   .arrow-icon-circle {
@@ -125,131 +100,4 @@ defineEmits(['prev', 'next', 'voice-recognized', 'recording-status-changed']);
     }
   }
 
-.voice-input-outer {
-  position: relative;
-  width: rpx(42);
-  height: rpx(42);
-  border-radius: 50%;
-  background: transparent;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  &::before {
-    content: '';
-    position: absolute;
-    width: 85%;
-    height: 85%;
-    border-radius: 50%;
-    border: rpx(2) solid rgba(80, 190, 240, 0.6);
-  }
-
-  &.recording {
-    &::before {
-      animation: pulse 2s infinite;
-      border-color: rgba(0, 100, 192, 0.6);
-    }
-
-    &::after {
-      content: '';
-      position: absolute;
-      width: 85%;
-      height: 85%;
-      border-radius: 50%;
-      border: rpx(2) solid rgba(0, 100, 192, 0.4);
-      animation: pulse 2s infinite 0.5s;
-    }
-
-    :deep(.voice-input-container) {
-      .speech-btn {
-        background: linear-gradient(135deg, #A0DCF0, #50BEF0);
-        border-color: rgba(0, 100, 192, 1);
-
-        .el-icon {
-          color: #0064BE;
-        }
-      }
-    }
-  }
-
-  &.placeholder {
-    visibility: hidden;
-  }
-
-  :deep(.voice-input-container) {
-    position: relative;
-    z-index: 10;
-
-    .speech-btn {
-      width: rpx(33);
-      height: rpx(33);
-      border-radius: 50%;
-      border: rpx(2) solid rgba(0, 100, 192);
-      background: linear-gradient(135deg, #A0DCF0, #50BEF0);
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      padding: 0;
-      gap: 0;
-      transition: all 0.3s ease;
-      cursor: pointer;
-      position: relative;
-      overflow: hidden;
-
-      &:hover {
-        transform: scale(1.05);
-        box-shadow: 0 rpx(4) rpx(12) rgba(0, 0, 0, 0.3);
-      }
-
-      &:active {
-        transform: scale(0.95);
-      }
-
-      &::before {
-        content: '';
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        width: 0;
-        height: 0;
-        border-radius: 50%;
-        background: rgba(255, 255, 255, 0.5);
-        transform: translate(-50%, -50%);
-        transition: width 0.6s, height 0.6s;
-      }
-
-      &:active::before {
-        width: rpx(80);
-        height: rpx(80);
-      }
-
-      .el-icon {
-        font-size: rpx(18);
-        color: #0064BE;
-        z-index: 1;
-      }
-
-      .countdown-text {
-        display: block;
-        font-size: rpx(8);
-        color: #0064BE;
-        position: absolute;
-        bottom: rpx(5);
-        left: 50%;
-        transform: translateX(-50%);
-      }
-    }
-  }
-}
-
-@keyframes pulse {
-  0% {
-    transform: scale(1);
-    opacity: 1;
-  }
-  100% {
-    transform: scale(1.15);
-    opacity: 0;
-  }
-}
 </style>