丸子 hace 8 meses
padre
commit
d46b260d69

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>ai通识课程平台</title>
+    <title>AI通识课程平台</title>
   </head>
   <body>
     <div id="app"></div>

+ 2 - 5
src/components/HomePage.vue

@@ -2,7 +2,7 @@
   <div class="home-container">
     <div class="box-1">
       <div class="inner-box left-box">
-        <span>京华实验学校</span>
+        <span>人工智能实验学校</span>
         <div class="dropdown-box">
           <!-- 下拉菜单 -->
           <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" popper-class="no-arrow-dropdown">
@@ -359,7 +359,7 @@ onMounted(() => {
 .left-box span {
   position: absolute;
   margin-top: rpx(20); // 调整上边距离
-  margin-left: rpx(25);
+  margin-left: rpx(30);
   font-size: rpx(11); // 默认字体大小
   color: white;
 }
@@ -397,9 +397,6 @@ onMounted(() => {
   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
 }
 .dropdown-box {
-  width: 100%;
-  height: 100%;
-  margin-left: rpx(-80);
   flex: 1;
   align-items: center; // 垂直居中;
   margin-top: rpx(22);

+ 18 - 26
src/components/Image/ImageView.vue

@@ -44,6 +44,8 @@
 import { defineProps, computed, ref, onMounted, onUnmounted } from 'vue'
 import videoImage01 from '@/assets/icon/videoImage01.png'
 import videoImage02 from '@/assets/icon/videoImage02.png'
+// 消息提示
+import { ElMessage } from 'element-plus'
 
 // 定义props
 const props = defineProps({
@@ -65,41 +67,31 @@ const currentIndex = ref(0)
 const carouselWrapper = ref(null)
 const autoplayTimer = ref(null)
 
-// 下一张图片
+// 下一张图片 - 取消循环切换,在最后一张时显示提示
 const nextSlide = () => {
-  currentIndex.value = (currentIndex.value + 1) % images.value.length
-  resetAutoplay()
+  if (currentIndex.value < images.value.length - 1) {
+    currentIndex.value++
+  } else {
+    // 已经是最后一张图片,显示提示信息
+    ElMessage.warning('已播放到最后一张图片')
+  }
 }
 
-// 上一张图片
+// 上一张图片 - 取消循环切换
 const prevSlide = () => {
-  currentIndex.value = (currentIndex.value - 1 + images.value.length) % images.value.length
-  resetAutoplay()
+  if (currentIndex.value > 0) {
+    currentIndex.value--
+  } else {
+    // 已经是第一张图片,显示提示信息
+    ElMessage.warning('已播放到第一张图片')
+  }
 }
 
 // 跳转到指定图片
 const goToSlide = (index) => {
   currentIndex.value = index
-  resetAutoplay()
-}
-
-// 重置自动播放计时器
-// const resetAutoplay = () => {
-//   if (!props.autoPlay) return
-  
-//   if (autoplayTimer.value) {
-//     clearInterval(autoplayTimer.value)
-//   }
-  
-//   autoplayTimer.value = setInterval(nextSlide, props.interval)
-// }
-
-// 自动播放设置
-// onMounted(() => {
-//   if (props.autoPlay) {
-//     resetAutoplay()
-//   }
-// })
+}
+
 
 // 清理定时器
 onUnmounted(() => {

+ 11 - 7
src/components/PPT/PptView.vue

@@ -28,10 +28,10 @@
         
         <!-- 轮播控制按钮 -->
         <div v-if="totalPages > 1" class="carousel-controls">
-          <button class="control-btn left-btn" @click="prevPage" :disabled="currentPage <= 1">
+          <button class="control-btn left-btn" @click="prevPage">
             <img :src="leftImg" alt="上一页" />
           </button>
-          <button class="control-btn right-btn" @click="nextPage" :disabled="currentPage >= totalPages">
+          <button class="control-btn right-btn" @click="nextPage">
             <img :src="rightImg" alt="下一页" />
           </button>
         </div>
@@ -62,7 +62,6 @@ const props = defineProps({
 
 // 修改PPT渲染完成处理
 const handlePptRendered = (pptInfo) => {
-  console.log('PPT渲染完成', pptInfo);
   // 获取总页数
   const pageCount = pptInfo?.slides?.length || 1;
   totalPages.value = pageCount;
@@ -74,7 +73,7 @@ const handlePptRendered = (pptInfo) => {
   scrollToPage(currentPage.value);
 }
 
-// 添加页面变更处理函数
+// 页面变更处理函数
 const handlePageChange = (newPage) => {
   // 验证页码有效性
   const normalizedPage = Math.max(1, Math.min(newPage + 1, totalPages.value));
@@ -85,7 +84,7 @@ const handlePageChange = (newPage) => {
 };
 
 
-// 新增滚动控制变量
+// 滚动控制变量
 const pptContainer = ref(null)
 const pageHeight = ref(0) // 单页高度,初始设为0
 const pptKey = ref(0) // 添加key用于重新渲染
@@ -102,18 +101,23 @@ const handleResize = () => {
   }
 }
 
+// 下一页
 const nextPage = () => {
   if (currentPage.value < totalPages.value) {
     currentPage.value++;
     scrollToPage(currentPage.value);
+  } else {
+    ElMessage.warning('已播放到最后一页')
   }
 }
 
-// 新增翻页方法
+// 上一页
 const prevPage = () => {
   if (currentPage.value > 1) {
     currentPage.value--;
     scrollToPage(currentPage.value);
+  }else {
+    ElMessage.warning('已播放到第一页')
   }
 }
 
@@ -123,7 +127,7 @@ const goToSlide = (index) => {
   scrollToPage(currentPage.value);
 }
 
-// 新增滚动到指定页方法
+// 滚动到指定页方法
 const scrollToPage = (pageNum) => {
   if (pptContainer.value) {
     // 计算目标滚动位置

+ 21 - 10
src/components/videopage/DialogComponents.vue

@@ -151,7 +151,7 @@
 </template>
 
 <script setup>
-import {ref, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
+import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
 import { ElMessage } from 'element-plus'
 import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
 import { teacherList } from '@/api/teachers.js'
@@ -199,7 +199,7 @@ const enableContext = ref(true)
 
 // tts
 import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
-const { playAudioChunk } = useAudioPlayer();
+const { playAudioChunk , stopPlayback  } = useAudioPlayer();
 
 // 语音输入响应式变量
 const isRecording = ref(false) // 录音状态
@@ -214,6 +214,7 @@ const handleSelectMessage = message => {
 
 // 关闭试题弹框
 const handleCloseQuestionDialog = () => {
+  stopPlayback(); // 销毁语音读取
   emits('closeQuestionDialog')
 }
 
@@ -223,7 +224,6 @@ const handleSubmitAnswer = () => {
     ElMessage.warning('请选择一个选项')
     return
   }
-
   emits('submitAnswer', { selectedOption: selectedOption.value })
   selectedOption.value = null
 }
@@ -239,7 +239,7 @@ const handleAIClick = async () => {
 
   if (props.currentQuestion.ccQuestContent) {
     // prompt.value = props.currentQuestion.ccQuestContent
-    // sendMessage()
+    sendMessage()
 
     prompt.value = ''
     // 执行发送
@@ -255,7 +255,6 @@ const handleAIClick = async () => {
 const getXzAi = async () => {
   try {
     const grade = localStorage.getItem('selectedGrade') || ''
-
     // 获取AI数据
     const juniorAIRes = await teacherList({ category: grade + 'AI' })
     const aiPerson = juniorAIRes.data.list.find(
@@ -303,7 +302,7 @@ const sendMessage = async () => {
 
     // 增加问答次数
     aiQuestionCount.value++
-    
+
     // 保存AI问答次数
     try {
       await saveRecord({
@@ -347,7 +346,7 @@ const initSpeechRecognition = () => {
     }
   }
 
-  // 新增:识别器结束时清除定时器
+  // 识别器结束时清除定时器
   instance.onend = () => {
     clearInterval(countdownTimer.value)
     isRecording.value = false
@@ -574,6 +573,8 @@ const stopStream = async () => {
   if (conversationInAbortController.value) {
     conversationInAbortController.value.abort()
   }
+  // 销毁语音读取
+  stopPlayback();
   // 设置为 false
   conversationInProgress.value = false
 }
@@ -615,6 +616,12 @@ watch(() => props.questionDialogVisible, (newVal) => {
     selectedOption.value = null
   }
 })
+// 监听showAIDialog变化,在关闭时销毁语音读取
+watch(() => showAIDialog.value, (newVal) => {
+  if (!newVal) {
+    stopPlayback();
+  }
+})
 // 监听消息列表变化,自动滚动到底部
 watch(messageList, () => {
   scrollToBottom()
@@ -648,6 +655,10 @@ const scrollToBottom = () => {
 onMounted(() => {
   // 初始化
 })
+// 组件卸载时清理语音资源
+onUnmounted(() => {
+  stopPlayback();
+});
 </script>
 
 <style scoped lang="scss">
@@ -790,8 +801,8 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
 
 // 底部按钮样式
 .child-button {
-  min-width: rpx(80);
-  height: rpx(30);
+  min-width: rpx(70);
+  height: rpx(25);
   border-radius: rpx(8);
   font-size: rpx(12);
   font-weight: 500;
@@ -833,7 +844,7 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
     width: rpx(30);
     height: rpx(30);
     margin-bottom: rpx(0);
-    filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
+    // filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
     // 添加过渡动画
     transition: transform 0.3s ease;
   }

+ 22 - 101
src/components/videopage/VideoPlayer.vue

@@ -13,20 +13,6 @@
         ></video>
       </template>
     </div>
-
-    <!-- 视频切换按钮 - 始终显示 -->
-    <!-- <div class="video-switch">
-      <div class="caret-left" @click="playPreviousVideo">
-        <el-button type="warning" round>
-          <img :src="leftImg" alt="Left" />上一节</el-button
-        >
-      </div>
-      <div class="caret-right" @click="playNextVideo">
-        <el-button type="warning" round
-          >下一节<img :src="rightImg" alt="Right" />
-        </el-button> 
-      </div>
-    </div> -->
   </div>
 </template>
 
@@ -184,30 +170,26 @@ const handleVideoEnded = () => {
   // emits('videoEnded')
 }
 
-// // 播放下一个视频
-// const playNextVideo = () => {
-//   const currentIndexInList = props.allIndices.indexOf(props.currentIndex)
-//   if (
-//     currentIndexInList !== -1 &&
-//     currentIndexInList < props.allIndices.length - 1
-//   ) {
-//     const nextIndex = props.allIndices[currentIndexInList + 1]
-//     emits('switchVideo', nextIndex)
-//     // 重置暂停索引
-//     pausedIndices.value = []
-//   }
-// }
-
-// // 播放上一个视频
-// const playPreviousVideo = () => {
-//   const currentIndexInList = props.allIndices.indexOf(props.currentIndex)
-//   if (currentIndexInList > 0) {
-//     const previousIndex = props.allIndices[currentIndexInList - 1]
-//     emits('switchVideo', previousIndex)
-//     // 重置暂停索引
-//     pausedIndices.value = []
-//   }
-// }
+// 在视频加载完成后设置上次播放进度
+const setLastPlayPosition = () => {
+  if (!videoRef.value) return
+  try {
+    const savedData = localStorage.getItem(`videoProgress_${props.courseId}`)
+    if (savedData) {
+      const { currentTime, progress } = JSON.parse(savedData)
+      if (currentTime && !isNaN(currentTime)) {
+        videoRef.value.currentTime = currentTime
+        lastPlayProgress.value = progress
+        // 检查是否已有保存的进度点
+        if (progress >= 10) savedProgress.value.push(10)
+        if (progress >= 50) savedProgress.value.push(50)
+        if (progress >= 100) savedProgress.value.push(100)
+      }
+    }
+  } catch (error) {
+    console.error('读取上次播放进度失败:', error)
+  }
+}
 
 // 初始化视频播放器
 const initVideoPlayer = () => {
@@ -268,26 +250,7 @@ const tryPlayVideo = () => {
   }, 1000)
 }
 
-// 在视频加载完成后设置上次播放进度
-const setLastPlayPosition = () => {
-  if (!videoRef.value) return
-  try {
-    const savedData = localStorage.getItem(`videoProgress_${props.courseId}`)
-    if (savedData) {
-      const { currentTime, progress } = JSON.parse(savedData)
-      if (currentTime && !isNaN(currentTime)) {
-        videoRef.value.currentTime = currentTime
-        lastPlayProgress.value = progress
-        // 检查是否已有保存的进度点
-        if (progress >= 10) savedProgress.value.push(10)
-        if (progress >= 50) savedProgress.value.push(50)
-        if (progress >= 100) savedProgress.value.push(100)
-      }
-    }
-  } catch (error) {
-    console.error('读取上次播放进度失败:', error)
-  }
-}
+
 
 // 组件挂载时
 onMounted(() => {
@@ -309,6 +272,7 @@ onBeforeUnmount(() => {
     hlsRef.value = null
   }
 })
+
 </script>
 
 <style scoped lang="scss">
@@ -424,47 +388,4 @@ onBeforeUnmount(() => {
 video::-webkit-media-controls-panel {
   background: transparent !important; /* 去掉背景渐变,设为透明 */
 }
-
-// .video-switch {
-//   width: 100%;
-//   display: flex;
-//   margin-top: rpx(5);
-//   margin-bottom: rpx(15);
-// }
-// .image-container {
-//   width: 100%;
-//   height: rpx(300);
-//   display: flex;
-//   justify-content: center;
-//   align-items: center;
-// }
-// .course-image {
-//   max-width: 70%;
-//   max-height: rpx(289);
-//   border-radius: rpx(12);
-//   object-fit: contain;
-// }
-// .caret-right,
-// .caret-left {
-//   width: rpx(50);
-//   margin: auto;
-//   display: flex;
-// }
-
-// .caret-left ::v-deep(.el-button.is-round),
-// .caret-right ::v-deep(.el-button.is-round) {
-//   width: rpx(50);
-//   height: rpx(15);
-//   color: white;
-//   font-size: rpx(7);
-//   border-radius: none;
-//   border: 1px white solid;
-//   background-color: rgb(255, 255, 255, 0.5);
-//   box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
-// }
-
-// .caret-right img,
-// .caret-left img {
-//   width: rpx(12);
-// }
 </style>

+ 10 - 8
src/views/AIDevelop.vue

@@ -432,6 +432,8 @@ onMounted(async () => {
     try {
       // 取接口课程数据
       const res = await ClassType(typeIdParam)
+      console.log(res);
+      
       courseList.value = res.data
       // 初始化已观看课程ID
       const savedWatchedIds = localStorage.getItem('watchedCourseIds')
@@ -450,14 +452,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]

+ 14 - 5
src/views/AIQuestions.vue

@@ -106,7 +106,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, computed, watch, nextTick } from "vue";
+import { ref, onMounted,onUnmounted, computed, watch, nextTick } from "vue";
 import { CreateDialogue, sendChatMessageStream } from "@/api/questions.js";
 import { useRouter, useRoute } from "vue-router";
 import { saveRecord } from "@/api/personalized/index.js";
@@ -156,7 +156,7 @@ const handleDefaultMessageSelect = (message) => {
   showDefaultMessages.value = false;
 };
 
-// 添加抽屉显示状态
+// 抽屉显示状态
 const drawerVisible = ref(true);
 // 添加切换抽屉显示状态的函数
 const toggleDrawer = () => {
@@ -169,6 +169,8 @@ const handleClose = () => {};
 
 // 返回上一页
 const goBack = () => {
+  // 停止语音播放
+  stopPlayback();
   router.push("/ai-laboratory");
 };
 const router = useRouter();
@@ -251,7 +253,7 @@ const initSpeechRecognition = () => {
     }
   };
 
-  // 新增:识别器结束时清除定时器
+  // 识别器结束时清除定时器
   instance.onend = () => {
     clearInterval(countdownTimer.value);
     isRecording.value = false;
@@ -420,7 +422,8 @@ const doSendMessage = async (content) => {
 
 import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
 
-const { playAudioChunk } = useAudioPlayer();
+// 解构 stopPlayback 方法
+const { playAudioChunk,stopPlayback } = useAudioPlayer();
 
 /** 真正执行【发送】消息操作 */
 const doSendMessageStream = async (userMessage) => {
@@ -450,7 +453,7 @@ const doSendMessageStream = async (userMessage) => {
 
     // 1.2 开始滚动
     textRoll();
-
+    
     // 2. 发送 event stream
     let isFirstChunk = true; // 是否是第一个 chunk 消息段
 
@@ -656,6 +659,8 @@ watch(
   (newQuery, oldQuery) => {
     // 只有当id变化时才更新数据,避免不必要的刷新
     if (newQuery.id && newQuery.id !== oldQuery?.id) {
+       // 停止语音播放
+      stopPlayback();
       // 更新相关数据
       personId.value = newQuery.id;
       personName.value = newQuery.name;
@@ -681,6 +686,10 @@ watch(
   },
   { immediate: true, deep: true }
 );
+// 组件卸载时清理语音资源
+onUnmounted(() => {
+  stopPlayback();
+});
 </script>
 
 <style scoped lang="scss">