丸子 před 3 měsíci
rodič
revize
66236c84fc

+ 3 - 1
src/components/blockly/MapGame.vue

@@ -178,7 +178,7 @@ const passConfig = ref({
 });
 
 // 定义emits
-const emits = defineEmits(['saveProgress'])
+const emits = defineEmits(['saveProgress', 'closeOverlay'])
 // 定义组件属性
 const props = defineProps({
   // 游戏ID
@@ -975,6 +975,8 @@ function showGameMessage(message, type = 'info', duration = CONFIG.DELAY.MESSAGE
 const closeOverlay = () => {
   gameState.player.hasReachedEnd = false;
   showOverlay.value = false;
+  // 发出关闭遮罩层事件,通知父组件
+  emits('closeOverlay');
 };
 
 //================积木组件方法=====================

+ 201 - 0
src/components/popup/PlayPrompt.vue

@@ -0,0 +1,201 @@
+<template>
+  <div v-if="visible" class="play-prompt-overlay">
+    <div class="play-prompt-container">
+      <button class="close-button" @click="handleClose">×</button>
+      <h3 class="prompt-title">即将跳转下一节</h3>
+      <div class="countdown-container">
+        <div class="countdown-circle">
+          <span class="countdown-number">{{ countdown }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, watch } from 'vue'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  duration: {
+    type: Number,
+    default: 5
+  },
+  message: {
+    type: String,
+    // default: '准备好开始下一节了吗?'
+  }
+})
+
+const emit = defineEmits(['countdownEnd', 'close'])
+
+const countdown = ref(props.duration)
+let countdownTimer = null
+
+const handleClose = () => {
+  stopCountdown()
+  emit('close')
+}
+
+const startCountdown = () => {
+  countdown.value = props.duration
+  
+  if (countdownTimer) {
+    clearInterval(countdownTimer)
+  }
+  
+  countdownTimer = setInterval(() => {
+    countdown.value--
+    
+    if (countdown.value <= 0) {
+      clearInterval(countdownTimer)
+      emit('countdownEnd')
+    }
+  }, 1000)
+}
+
+const stopCountdown = () => {
+  if (countdownTimer) {
+    clearInterval(countdownTimer)
+    countdownTimer = null
+  }
+}
+
+watch(() => props.visible, (newVisible) => {
+  if (newVisible) {
+    startCountdown()
+  } else {
+    stopCountdown()
+  }
+})
+
+onMounted(() => {
+  if (props.visible) {
+    startCountdown()
+  }
+})
+
+onUnmounted(() => {
+  stopCountdown()
+})
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.play-prompt-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.7);
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+  padding-top: rpx(10);
+  z-index: 9999;
+}
+
+.play-prompt-container {
+  background: linear-gradient(135deg, #ffffff, #f0f0f0);
+  border-radius: rpx(10);
+  padding: rpx(6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: rpx(5);
+  box-shadow: 0 rpx(10) rpx(20) rgba(0, 0, 0, 0.3);
+  max-width: 80%;
+  animation: fadeInScale 0.5s ease-out;
+  position: relative;
+}
+
+.close-button {
+  width: rpx(10);
+  height: rpx(10);
+  border: none;
+  background: none;
+  font-size: rpx(10);
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  outline: none;
+  box-shadow: none;
+  padding: 0;
+  margin: 0;
+}
+
+.prompt-title {
+  font-size: rpx(8);
+  color: #333;
+  margin: 0;
+  font-weight: bold;
+}
+
+.prompt-message {
+  font-size: rpx(7);
+  color: #666;
+  margin-bottom: rpx(30);
+}
+
+.countdown-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.countdown-circle {
+  width: rpx(20);
+  height: rpx(20);
+  border-radius: 50%;
+  background: linear-gradient(135deg, #6a5acd, #9370db);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  box-shadow: 0 rpx(10) rpx(20) rgba(106, 90, 205, 0.4);
+  animation: pulse 1s infinite;
+}
+
+.countdown-number {
+  font-size: rpx(10);
+  font-weight: bold;
+  color: white;
+  text-shadow: 0 rpx(2) rpx(4) rgba(0, 0, 0, 0.3);
+}
+
+// 动画效果
+@keyframes fadeInScale {
+  from {
+    opacity: 0;
+    transform: scale(0.8);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.05);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+</style><!-- 即将播放下一节提示 -->

+ 114 - 0
src/components/popup/PromptPopup.vue

@@ -0,0 +1,114 @@
+<template>
+  <div v-if="visible" class="prompt-popup-overlay">
+    <div class="prompt-popup-content">
+      <!-- <h3 class="prompt-title">提示</h3> -->
+      <p class="prompt-message">已经播放完最后一节,是否返回列表页?</p>
+      <div class="prompt-buttons">
+        <button class="prompt-btn cancel" @click="handleCancel">取消</button>
+        <button class="prompt-btn confirm" @click="handleConfirm">确定</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue';
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(['confirm', 'cancel']);
+
+const handleConfirm = () => {
+  emit('confirm');
+};
+
+const handleCancel = () => {
+  emit('cancel');
+};
+
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.prompt-popup-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+  padding-top: rpx(10);
+  z-index: 1000;
+}
+
+.prompt-popup-content {
+  background-color: white;
+  border-radius: rpx(4);
+  padding: rpx(5);
+  width: 90%;
+  max-width: rpx(160);
+  box-shadow: 0 rpx(2) rpx(6) rgba(0, 0, 0, 0.15);
+}
+
+.prompt-title {
+  margin-top: 0;
+  margin-bottom: rpx(8);
+  font-size: rpx(9);
+  font-weight: 600;
+  color: #333;
+  text-align: center;
+}
+
+.prompt-message {
+  margin-bottom: rpx(12);
+  font-size: rpx(8);
+  color: #666;
+  text-align: center;
+  line-height: 1.5;
+}
+
+.prompt-buttons {
+  display: flex;
+  justify-content: flex-end;
+  gap: rpx(6);
+}
+
+.prompt-btn {
+  padding: rpx(4) rpx(8);
+  border: none;
+  border-radius: rpx(2);
+  font-size: rpx(7);
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.prompt-btn.cancel {
+  background-color: #f0f0f0;
+  color: #333;
+}
+
+.prompt-btn.cancel:hover {
+  background-color: #e0e0e0;
+}
+
+.prompt-btn.confirm {
+  background: linear-gradient(135deg, #6a5acd, #9370db);
+  color: white;
+}
+
+.prompt-btn.confirm:hover {
+  background: linear-gradient(135deg, #6a5acd, #9370db);
+}
+</style>

+ 5 - 0
src/components/videopage/VideoPlayer.vue

@@ -345,6 +345,11 @@ const tryPlayVideo = () => {
     return
   }
 
+  // 自动播放视频
+  videoRef.value.play().catch(error => {
+    console.error('自动播放失败:', error)
+  })
+
   // 在视频加载完成后设置上次播放进度
   setTimeout(() => {
     setLastPlayPosition()

+ 56 - 5
src/views/AIPage/AIDevelop.vue

@@ -200,6 +200,20 @@
       @saveProgress="handleSaveProgress"
     />
 
+    <!-- 提示弹窗组件 -->
+    <PromptPopup
+      :visible="promptPopupVisible"
+      @confirm="handlePromptConfirm"
+      @cancel="handlePromptCancel"
+    />
+
+    <!-- 播放提示组件 -->
+    <PlayPrompt
+      :visible="playPromptVisible"
+      @countdownEnd="handlePlayPromptEnd"
+      @close="handlePlayPromptClose"
+    />
+
   </div>
 </template>
 
@@ -226,6 +240,9 @@ import VideoPlayer from '@/components/videopage/VideoPlayer.vue'
 import DialogComponents from '@/components/videopage/DialogComponents.vue'
 import PptView from "@/components/PPT/PptView.vue";
 import ImageView from '@/components/Image/ImageView.vue'
+import PromptPopup from '@/components/popup/PromptPopup.vue'
+import PlayPrompt from '@/components/popup/PlayPrompt.vue'
+
 
 // AI实验室
 import TextToText from "@/components/ai/text/TextToText.vue";
@@ -268,6 +285,11 @@ const typeSort = ref('')
 // 测试账号禁用视频
 const isDisabled = ref(false)
 
+// 最后一节提示弹窗显示状态
+const promptPopupVisible = ref(false)
+// 即将播放下一节提示显示状态
+const playPromptVisible = ref(false)
+
 // 保存视频进度接口
 const handleSaveProgress = async (type, progress) => {
 
@@ -394,11 +416,16 @@ const handleVideoEnded = () => {
   }
 
   // 自动播放下一个
-  const allIndices = flattenMenuItems()
-  const currentIndexInList = allIndices.indexOf(course.value.key)
-  if (currentIndexInList !== -1 && currentIndexInList < allIndices.length - 1) {
-    const nextIndex = allIndices[currentIndexInList + 1]
-    handleSelect(nextIndex)
+  // 检查是否是最后一节
+  if (courseList.value && courseList.value.length > 0) {
+    const currentIndex = courseList.value.findIndex(item => item.id === course.value.id)
+    if (currentIndex === courseList.value.length - 1) {
+      // 显示提示弹窗
+      promptPopupVisible.value = true
+    } else {
+      // 显示播放提示
+      playPromptVisible.value = true
+    }
   }
 }
 
@@ -462,6 +489,30 @@ const handleSubmitAnswer = ({ selectedOption }) => {
   questionDialogVisible.value = false
 }
 
+// 处理提示弹窗确定按钮
+const handlePromptConfirm = () => {
+  promptPopupVisible.value = false
+  // 返回列表页
+  goBack()
+}
+
+// 处理提示弹窗取消按钮
+const handlePromptCancel = () => {
+  promptPopupVisible.value = false
+}
+
+// 处理播放提示倒计时结束
+const handlePlayPromptEnd = () => {
+  playPromptVisible.value = false
+  // 自动播放下一个视频
+  playNextVideo();
+}
+
+// 处理播放提示关闭
+const handlePlayPromptClose = () => {
+  playPromptVisible.value = false
+}
+
 // 搜索
 const querySearch = (queryString, cb) => {
   const sections = getAllCourseSections()

+ 60 - 2
src/views/laboratory/ExperimentalInterface.vue

@@ -123,6 +123,20 @@
       @saveProgress="handleSaveProgress"
     />
 
+    <!-- 提示弹窗组件 -->
+    <PromptPopup
+      :visible="promptPopupVisible"
+      @confirm="handlePromptConfirm"
+      @cancel="handlePromptCancel"
+    />
+
+    <!-- 播放提示组件 -->
+    <PlayPrompt
+      :visible="playPromptVisible"
+      @countdownEnd="handlePlayPromptEnd"
+      @close="handlePlayPromptClose"
+    />
+
   </div>
 </template>
 
@@ -143,6 +157,8 @@ import VideoPlayer from '@/components/videopage/VideoPlayer.vue'
 import DialogComponents from '@/components/videopage/DialogComponents.vue'
 import PptView from "@/components/PPT/PptView.vue";
 import ImageView from '@/components/Image/ImageView.vue'
+import PromptPopup from '@/components/popup/PromptPopup.vue'
+import PlayPrompt from '@/components/popup/PlayPrompt.vue'
 // AI实验室
 import TextToText from "@/components/ai/text/TextToText.vue";
 import TextToImage from "@/components/ai/image/TextToImage.vue";
@@ -192,6 +208,10 @@ const watchedCourseIds = ref([])
 const questionDialogVisible = ref(false)
 // 当前显示的试题
 const courseConfig = ref({})
+// 最后一节提示弹窗显示状态
+const promptPopupVisible = ref(false)
+// 即将播放下一节提示显示状态
+const playPromptVisible = ref(false)
 // 年级id
 const gradeId = ref('')
 // 课程大纲id
@@ -280,8 +300,22 @@ const handleVideoEnded = () => {
     )
   }
 
-  // 自动播放下一个
-  // playNextVideo();
+  // 检查是否是最后一节
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = currentCourseIndex.value
+    if (currentIndex === props.courseList.length - 1) {
+      // 显示提示弹窗
+      promptPopupVisible.value = true
+    } else {
+      // 只有在视频类型时才显示播放提示
+      if (course.value.courseContentType === 'video') {
+        playPromptVisible.value = true
+      } else {
+        // 非视频类型直接播放下一个
+        playNextVideo();
+      }
+    }
+  }
 }
 
 // 处理视频时间更新事件
@@ -325,6 +359,30 @@ const handleSubmitAnswer = ({ selectedOption }) => {
   questionDialogVisible.value = false
 }
 
+// 处理提示弹窗确定按钮
+const handlePromptConfirm = () => {
+  promptPopupVisible.value = false
+  // 触发关闭视频事件,返回列表页
+  emit('closeVideo')
+}
+
+// 处理提示弹窗取消按钮
+const handlePromptCancel = () => {
+  promptPopupVisible.value = false
+}
+
+// 处理播放提示倒计时结束
+const handlePlayPromptEnd = () => {
+  playPromptVisible.value = false
+  // 自动播放下一个视频
+  playNextVideo();
+}
+
+// 处理播放提示关闭
+const handlePlayPromptClose = () => {
+  playPromptVisible.value = false
+}
+
 // 处理父组件传递的课程数据
 const handleParentCourseData = (courseData = props.courseData) => {
   if (!courseData) return false

+ 68 - 2
src/views/programming/Interface.vue

@@ -97,6 +97,7 @@
                    @close-game="emit('closeVideo')"
                    @prev-section="playPreviousVideo"
                    @next-section="playNextVideo"
+                   @close-overlay="handleMapGameCloseOverlay"
                    @saveProgress="handleSaveProgress"
           ></MapGame>
 
@@ -132,6 +133,20 @@
       @saveProgress="handleSaveProgress"
     />
 
+    <!-- 提示弹窗组件 -->
+    <PromptPopup
+      :visible="promptPopupVisible"
+      @confirm="handlePromptConfirm"
+      @cancel="handlePromptCancel"
+    />
+
+    <!-- 播放提示组件 -->
+    <PlayPrompt
+      :visible="playPromptVisible"
+      @countdownEnd="handlePlayPromptEnd"
+      @close="handlePlayPromptClose"
+    />
+
   </div>
 </template>
 
@@ -158,6 +173,8 @@ import VideoPlayer from '@/components/videopage/VideoPlayer.vue'
 import DialogComponents from '@/components/videopage/DialogComponents.vue'
 import PptView from "@/components/PPT/PptView.vue";
 import ImageView from '@/components/Image/ImageView.vue'
+import PromptPopup from '@/components/popup/PromptPopup.vue'
+import PlayPrompt from '@/components/popup/PlayPrompt.vue'
 
 // AI实验室
 import TextToText from "@/components/ai/text/TextToText.vue";
@@ -197,6 +214,10 @@ const watchedCourseIds = ref([])
 const questionDialogVisible = ref(false)
 // 当前显示的试题
 const courseConfig = ref({})
+// 最后一节提示弹窗显示状态
+const promptPopupVisible = ref(false)
+// 即将播放下一节提示显示状态
+const playPromptVisible = ref(false)
 // 年级id
 const gradeId = ref('')
 // 课程大纲id
@@ -269,8 +290,24 @@ const handleVideoEnded = () => {
     )
   }
 
-  // 自动播放下一个
-  // playNextVideo();
+  // 检查是否是最后一节
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
+    if (currentIndex === props.courseList.length - 1) {
+      // 显示提示弹窗
+      promptPopupVisible.value = true
+    } else {
+      // 显示播放提示
+      playPromptVisible.value = true
+    }
+  }
+}
+
+// 处理播放提示倒计时结束
+const handlePlayPromptEnd = () => {
+  playPromptVisible.value = false
+  // 自动播放下一个视频
+  playNextVideo();
 }
 
 // 处理视频时间更新事件
@@ -320,6 +357,35 @@ const handleSubmitAnswer = ({ selectedOption }) => {
   questionDialogVisible.value = false
 }
 
+// 处理提示弹窗确定按钮
+const handlePromptConfirm = () => {
+  promptPopupVisible.value = false
+  // 触发关闭视频事件,返回列表页
+  emit('closeVideo')
+}
+
+// 处理提示弹窗取消按钮
+const handlePromptCancel = () => {
+  promptPopupVisible.value = false
+}
+
+// 处理播放提示关闭
+const handlePlayPromptClose = () => {
+  playPromptVisible.value = false
+}
+
+// 处理 MapGame 组件关闭遮罩层事件
+const handleMapGameCloseOverlay = () => {
+  // 检查是否是最后一节
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
+    if (currentIndex !== props.courseList.length - 1) {
+      // 显示播放提示,准备开始下一节
+      playPromptVisible.value = true
+    }
+  }
+}
+
 // 处理父组件传递的课程数据
 const handleParentCourseData = (courseData = props.courseData) => {
   if (!courseData) return false;