Răsfoiți Sursa

新编程界面的视频和游戏

丸子 7 luni în urmă
părinte
comite
0691dc4578

+ 4 - 4
src/router/index.js

@@ -89,10 +89,10 @@ const routes = [
   { path: '/blockly2', component: () => import('../views/block/Blockly2.vue') },
   { path: '/mapGame', component: () => import('../views/block/MapGame.vue') },
   // 编程游戏列表
-  { path: '/programming', component: () => import('../views/gamepage/GameIndex.vue') },
+  { path: '/programming02', component: () => import('../views/gamepage/GameIndex.vue') },
   // 编程课
   {
-    path: '/programming02', 
+    path: '/programming', 
     component: () => import('../views/programming/ProgrammingGame.vue')
   },
   // 编程课列表
@@ -107,8 +107,8 @@ const routes = [
   },
   // 编程课视频
   {
-    path: '/programmingvideo', 
-    component: () => import('../views/programming/ProgrammingVideo.vue')
+    path: '/interface', 
+    component: () => import('../views/programming/Interface.vue')
   },
 ]
 const router = createRouter({

+ 220 - 94
src/views/block/MapGame.vue

@@ -1,13 +1,32 @@
 <template>
   <div class="map-game-container">
-    <!-- 标题栏 -->
     <div class="title-box">
-      <div class="box-icon" @click="navigateBack">
-        <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
-        {{ gameTitle }}
+      <!-- 左侧标题部分 -->
+      <div class="left-container">
+        <div class="box-icon" @click="navigateBack">
+          <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+          {{ gameTitle }}
+        </div>
+        <!-- 游戏编号 -->
+        <div class="game-badge">{{ gameSort }}</div>
+      </div>
+      <!-- 右侧上下节按钮 -->
+      <div class="right-container">
+        <button 
+          class="section-button previous-btn" 
+          @click="handlePreviousSection"
+          v-if="props.currentIndex > 0"
+        >
+          上一节
+        </button>
+        <button 
+          class="section-button next-btn" 
+          @click="handleNextSection"
+          v-if="props.currentIndex < props.courseList.length - 1"
+        >
+          下一节
+        </button>
       </div>
-      <!-- 游戏编号 -->
-      <div class="game-badge">{{ gameSort }}</div>
     </div>
 
     <div class="content">
@@ -119,6 +138,73 @@ import 'blockly/msg/zh-hans';
 import { javascriptGenerator } from "blockly/javascript";
 import playerImage from '@/assets/images/blockly/user.png';
 
+// 定义组件属性
+const props = defineProps({
+  // 游戏ID
+  gameId: {
+    type: [String, Number],
+    default: ''
+  },
+  // 地图背景
+  mapBackground: {
+    type: String,
+    default: ''
+  },
+  // 地图瓦片大小
+  mapTileSize: {
+    type: [String, Object],
+    default: ''
+  },
+  // 地图起点
+  mapStartPoint: {
+    type: [String, Object],
+    default: ''
+  },
+  // 地图终点
+  mapEndPoint: {
+    type: [String, Object],
+    default: ''
+  },
+  // 可行走点
+  mapWalkablePoints: {
+    type: [String, Array],
+    default: ''
+  },
+  // 用户方向
+  userDirection: {
+    type: Number,
+    default: 0
+  },
+  // 用户图片
+  userImage: {
+    type: String,
+    default: ''
+  },
+  // 游戏信息
+  info: {
+    type: String,
+    default: ''
+  },
+  // 游戏标题
+  gameTitle: {
+    type: String,
+    default: ''
+  },
+  // 课程列表
+  courseList: {
+    type: Array,
+    default: () => []
+  },
+  // 当前课程索引
+  currentIndex: {
+    type: Number,
+    default: 0
+  }
+});
+
+// 定义emit事件
+const emit = defineEmits(['closeGame', 'prevSection', 'nextSection']);
+
 // 游戏接口数据
 import { getMapGameById } from '@/api/blockly/game.js';
 
@@ -173,7 +259,7 @@ const CONFIG = {
 // 路由和游戏状态
 const router = useRouter();
 const route = useRoute();
-const gameTitle = ref('地图游戏编程'); // 默认标题
+const gameTitle = computed(() => props.gameTitle);
 const currentGameData = ref(null);
 const playerInitialDirection = ref(0); // 人物初始朝向
 const gameSort = ref('Game1'); // 默认排序
@@ -288,47 +374,6 @@ const tileSize = computed(() => {
   return Math.min(tileWidth, tileHeight);
 });
 
-// 初始化Blockly工作区
-function initBlockly() {
-  // 在初始化前设置语言环境
-  // Blockly 12.x版本推荐在导入后立即设置
-  Blockly.setLocale('zh-hans');
-  
-  const toolbox = document.getElementById('toolbox');
-  workspace = Blockly.inject('blocklyDiv', {
-    toolbox: toolbox,
-    collapse: false,
-    comments: true,
-    disable: false, // 设为false以允许编辑
-    maxBlocks: Infinity,
-    trashcan: true,
-    horizontalLayout: false,
-    toolboxPosition: 'start',
-    toolboxCollapse: false, // 设置工具箱默认展开
-    toolboxAlwaysExpanded: true, // 确保点击类别后保持展开状态
-    css: true,
-    media: 'https://unpkg.com/blockly/media/',
-    rtl: false,
-    scrollbars: true,
-    sounds: false, // 禁用声音以提高性能
-    oneBasedIndex: true,
-    locale: 'zh-hans', // 在配置中直接指定语言
-    grid: {
-      spacing: 20,
-      length: 3,
-      colour: "#ccc",
-      snap: true
-    },
-    zoom: {
-      controls: true,
-      wheel: true,
-      startScale: 1.0,
-      maxScale: 3,
-      minScale: 0.3,
-      scaleSpeed: 1.2
-    }
-  });
-}
 
 // 生命周期钩子
 onMounted(async () => {
@@ -355,32 +400,55 @@ onMounted(async () => {
 // 获取游戏数据
 const fetchGameData = async () => {
   try {
-    const gameId = route.query.gameId;
-    const nameFromRoute = route.query.gameName;
-    const sortFromRoute = route.query.gameSort;
+    // 优先使用props传递的数据
+    if (props.gameId && props.mapBackground) {
+      // 使用props数据构建游戏数据对象
+      currentGameData.value = {
+        mapBackground: props.mapBackground,
+        mapTileSize: props.mapTileSize,
+        mapStartPoint: props.mapStartPoint,
+        mapEndPoint: props.mapEndPoint,
+        mapWalkablePoints: props.mapWalkablePoints,
+        userDirection: props.userDirection,
+        userImage: props.userImage,
+        info: props.info
+      };
+      
+      // 直接更新游戏状态
+      await updateGameStateFromData(currentGameData.value);
+      
+      // 数据更新后强制刷新容器尺寸(等待DOM更新)
+      await nextTick();
+      updateMapContainerDimensions();
+    } else {
+      // 如果没有props数据,则从API获取
+      const gameId = route.query.gameId || props.gameId;
+      const nameFromRoute = route.query.gameName;
+      const sortFromRoute = route.query.gameSort;
 
-    if (nameFromRoute) {
-      gameTitle.value = nameFromRoute;
-    }
-    if (sortFromRoute) {
-      gameSort.value = sortFromRoute;
-    }
+      if (nameFromRoute) {
+        gameTitle.value = nameFromRoute;
+      }
+      if (sortFromRoute) {
+        gameSort.value = sortFromRoute;
+      }
 
-    let mapGameData = await getMapGameById(gameId);
-    if (mapGameData.code === 0) {
-      currentGameData.value = mapGameData?.data;
+      let mapGameData = await getMapGameById(gameId);
+      if (mapGameData.code === 0) {
+        currentGameData.value = mapGameData?.data;
 
-      // 使用接口数据
-      if (currentGameData.value && currentGameData.value.sort) {
-        let sortNum = currentGameData.value.sort;
-        gameSort.value = sortNum > 9 ? `Game${sortNum}` : `Game${sortNum}`;
-      }
+        // 使用接口数据
+        if (currentGameData.value && currentGameData.value.sort) {
+          let sortNum = currentGameData.value.sort;
+          gameSort.value = sortNum > 9 ? `Game${sortNum}` : `Game${sortNum}`;
+        }
 
-      await updateGameStateFromData(currentGameData.value);
+        await updateGameStateFromData(currentGameData.value);
 
-      // 数据更新后强制刷新容器尺寸(等待DOM更新)
-      await nextTick();
-      updateMapContainerDimensions();
+        // 数据更新后强制刷新容器尺寸(等待DOM更新)
+        await nextTick();
+        updateMapContainerDimensions();
+      }
     }
   } catch (error) {
     console.error('获取游戏数据失败:', error);
@@ -496,11 +564,22 @@ function getCarriedItemStyle(index, item) {
 
 // 导航返回
 function navigateBack() {
-  router.back();
+  emit('closeGame');
 }
 
+// 上一节按钮点击事件
+function handlePreviousSection() {
+  if (props.currentIndex > 0) {
+    emit('prevSection');
+  }
+}
 
-
+// 下一节按钮点击事件
+function handleNextSection() {
+  if (props.currentIndex < props.courseList.length - 1) {
+    emit('nextSection');
+  }
+}
 
 // 注册自定义积木
 function registerCustomBlocks() {
@@ -638,7 +717,8 @@ function registerJavaScriptGenerators() {
     })()`;
 
     // 获取循环体代码
-    const branch = javascriptGenerator.statementToCode(block, 'DO');
+    // const branch = javascriptGenerator.statementToCode(block, 'DO');
+    const branch = ""
 
     // 生成支持异步的循环代码
     let code = `for (let i = 0; i < ${safeRepeats}; i++) {\n`;
@@ -649,25 +729,25 @@ function registerJavaScriptGenerators() {
   };
 
   // 为while/until循环块注册自定义生成器
-  javascriptGenerator.forBlock['controls_whileUntil'] = function(block) {
-    const until = block.getFieldValue('MODE') === 'UNTIL';
-    const condition = javascriptGenerator.valueToCode(block, 'CONDITION',
-        javascriptGenerator.ORDER_NONE) || 'false';
-    const branch = javascriptGenerator.statementToCode(block, 'DO');
-
-    // 修复变量作用域问题,使用IIFE包装循环
-    let code = '(async function() {\n';
-    code += '  let loopCount = 0;\n';
-    code += until ? '  while (!((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n' :
-        '  while (((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n';
-    code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT + '  ');
-    code += '    loopCount++';
-    code += '    await new Promise(resolve => setTimeout(resolve, ' + CONFIG.DELAY.LOOP_PREVENTION + '));\n'; // 防止UI阻塞
-    code += '  }\n';
-    code += '})();\n';
-
-    return code;
-  };
+  // javascriptGenerator.forBlock['controls_whileUntil'] = function(block) {
+  //   const until = block.getFieldValue('MODE') === 'UNTIL';
+  //   const condition = javascriptGenerator.valueToCode(block, 'CONDITION',
+  //       javascriptGenerator.ORDER_NONE) || 'false';
+  //   const branch = javascriptGenerator.statementToCode(block, 'DO');
+
+  //   // 修复变量作用域问题,使用IIFE包装循环
+  //   let code = '(async function() {\n';
+  //   code += '  let loopCount = 0;\n';
+  //   code += until ? '  while (!((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n' :
+  //       '  while (((' + condition + ')) && loopCount < ' + CONFIG.GAME.MAX_LOOP_COUNT + ') {\n';
+  //   code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT + '  ');
+  //   code += '    loopCount++';
+  //   code += '    await new Promise(resolve => setTimeout(resolve, ' + CONFIG.DELAY.LOOP_PREVENTION + '));\n'; // 防止UI阻塞
+  //   code += '  }\n';
+  //   code += '})();\n';
+
+  //   return code;
+  // };
 
   // 为text_print块添加生成器,用于调试
   javascriptGenerator.forBlock['text_print'] = function(block) {
@@ -1621,7 +1701,6 @@ onUnmounted(() => {
 
 <style scoped lang="scss">
 @use "sass:math";
-
 @function rpx($px) {
   @return math.div($px, 750) * 100vw;
 }
@@ -1639,7 +1718,7 @@ onUnmounted(() => {
   bottom: 0;
   background: transparent;
   overflow-y: auto;
-  background-color: rgba(255, 255, 255);
+  background-image: url('@/assets/programming/list_bg03.png');
 }
 
 /* 自定义滚动条样式 */
@@ -1712,15 +1791,24 @@ onUnmounted(() => {
   top: rpx(5);
   padding-left: 15px;
   z-index: 10;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+/* 左侧容器样式 */
+.left-container {
   display: flex;
   flex-direction: column;
+  align-items: flex-start;
+  gap: 10px;
 }
 
 /* 右侧两个角为圆角的长方形格子样式 */
 .game-badge {
   width: rpx(80);
   height: rpx(20);
-  margin-left: rpx(10);
+  margin-left: rpx(3);
   background-color: #5fb5dc;
   color: #fff;
   border-radius: 0 rpx(20) rpx(20) 0;
@@ -1731,6 +1819,44 @@ onUnmounted(() => {
   font-weight: bold;
 }
 
+/* 右侧容器样式 */
+.right-container {
+  display: flex;
+  gap: 10px;
+  padding-right: 20px;
+}
+
+/* 上下节按钮样式 */
+.section-button {
+  padding: rpx(5) rpx(12);
+  border: none;
+  border-radius: rpx(10);
+  background: #3498db;
+  color: #fff;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-size: rpx(7);
+}
+
+.section-button:hover {
+  background: #2980b9;
+  transform: translateY(-2px);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
+}
+.previous-btn {
+    background-color: rgba(255, 255, 255, 0.8);
+    color: #333;
+}
+.previous-btn:hover {
+    background-color: rgba(255, 255, 255, 0.9);
+}
+.next-btn {
+  background: #5fb5dc;
+}
+.next-btn:hover {
+  background: #2980b9;
+}
 .box-icon {
   display: flex;
   align-items: center;

+ 270 - 415
src/views/programming/ProgrammingVideo.vue → src/views/programming/Interface.vue

@@ -1,102 +1,14 @@
 <template>
   <!-- 编程课程视频页面 -->
   <div class="home-container">
-    <!-- 展开收起侧边栏 -->
-    <div
-      class="icon-expand"
-      :style="{
-        backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
-        left: drawerVisible ? '18%' : '0',
-      }"
-      @click="toggleDrawer"
-    >
-      <span
-        class="vertical-lines"
-        :style="{
-          color: drawerVisible ? '#8a78d0' : 'white'
-        }"
-        >||</span
-      >
-    </div>
-
-    <el-drawer
-      v-model="drawerVisible"
-      direction="ltr"
-      size="18%"
-      :with-header="false"
-    >
-      <!-- 添加抽屉 -->
-      <div class="drawer-box">
-        <el-row class="tac">
-          <el-col :span="12">
-            <span class="mb-2">
-              <img :src="classImages" alt="课程小节图标" />
-              课程小节
-            </span>
-            <el-menu
-              :default-active="course.key"
-              @open="handleOpen"
-              @close="handleClose"
-              @select="handleSelect"
-              :default-openeds="['3','5']"
-            >
-              <template v-for="item in menuItems" :key="item.key">
-                <el-menu-item v-if="!item.children" :index="item.key">{{
-                  item.title
-                }}</el-menu-item>
-                <el-sub-menu v-else :index="item.key">
-                  <template #title>
-                    <span>{{ item.title }}</span>
-                  </template>
-                  <el-menu-item-group v-if="item.children">
-                    <template v-for="child in item.children" :key="child.key">
-                      <el-menu-item :index="child.key"
-                        >•{{ child.title }}</el-menu-item>
-                      >
-                    </template>
-                  </el-menu-item-group>
-                </el-sub-menu>
-              </template>
-            </el-menu>
-          </el-col>
-        </el-row>
-      </div>
-    </el-drawer>
-
     <div class="content-box">
       <div class="box-1">
         <div class="inner-box left-box">
-          <div class="box-icon" @click="goBack">
-            <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+          <div class="box-icon">
+            <el-icon class="left-icon" @click="emit('closeVideo')"><ArrowLeftBold /></el-icon>
             {{ boxIconTitle }}
           </div>
         </div>
-        <div class="inner-box right-box">
-          <div class="top-right-box">
-            <el-autocomplete
-              v-model="SearchInput"
-              :fetch-suggestions="querySearch"
-              placeholder="搜索"
-              @select="handleSearchSelect"
-              class="search-input"
-              value-key="title"
-              :trigger-on-focus="false"
-            >
-              <template #prefix>
-                <el-icon class="el-input__icon"><search /></el-icon>
-              </template>
-              <template #popper-append-to-body>
-                <el-option
-                  class="scrollbar"
-                  v-for="item in filteredTitles"
-                  :key="item.key"
-                  :label="item.title"
-                  :value="item"
-                ></el-option>
-              </template>
-            </el-autocomplete>
-          </div>
-        </div>
       </div>
 
       <div class="box-2">
@@ -124,7 +36,7 @@
               :currentIndex="course.key || ''"
               @timeUpdate="handleVideoTimeUpdate"
               @videoEnded="handleVideoEnded"
-              @switchVideo="handleSelect"
+              @switchVideo="switchVideo"
           />
           <!-- 图片 -->
           <ImageView v-if="course.courseContentType === 'image'" :imagePath="course.courseImagePath" altText="课程图片"></ImageView>
@@ -143,6 +55,26 @@
 
           <!--图生视频-->
           <ImageToVideo class="contentClass" v-if="course.courseContentType === 'aiImageToVideo'" ref="aiImageToVideo"></ImageToVideo>
+          
+          <!--编程地图游戏-->
+          <MapGame v-if="course.courseContentType === 'blockly'" 
+                   :game-id="course.id"
+                   :map-background="course.blocklyBackground"
+                   :map-tile-size="course.blocklyTileSize"
+                   :map-start-point="course.blocklyStartPoint"
+                   :map-end-point="course.blocklyEndPoint"
+                   :map-walkable-points="course.blocklyWalkablePoints"
+                   :user-direction="course.blocklyUserDirection"
+                   :user-image="course.blocklyUserImage"
+                   :info="course.blocklyInfo"
+                   :game-title="course.courseName"
+                   :course-list="props.courseList"
+                   :current-index="props.courseList.findIndex(item => item.id === course.id)"
+                   @close-game="emit('closeVideo')"
+                   @prev-section="playPreviousVideo"
+                   @next-section="playNextVideo"
+          ></MapGame>
+          
         </template>
 
         <!-- 视频切换按钮 - 始终显示 -->
@@ -178,13 +110,11 @@
 
 <script setup>
 import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import { Expand, Fold, Memo } from '@element-plus/icons-vue'
+import {  useRouter } from 'vue-router'
 import { Search, ArrowLeftBold } from '@element-plus/icons-vue'
-import { ElMessage, ElMessageBox, ElNotification, valueEquals } from 'element-plus'
+import { ElMessage } from 'element-plus'
 import isDisabledImage from '@/assets/images/permission/isDisabled.png'
 
-import classImages from '@/assets/icon/class.png'
 import { ClassType } from '@/api/class.js'
 import { Message } from '@/utils/message/Message.js'
 import { saveRecord } from '@/api/personalized/index.js'
@@ -207,19 +137,29 @@ import TextToText from "@/components/ai/text/TextToText.vue";
 import TextToImage from "@/components/ai/image/TextToImage.vue";
 import ImageToImage from "@/components/ai/image/ImageToImage.vue";
 import ImageToVideo from "@/components/ai/video/ImageToVideo.vue";
+import MapGame from "@/views/block/MapGame.vue";
 
 const router = useRouter() // 获取当前路由对象
-const route = useRoute()
-
-// 添加按钮显示状态
-const buttonVisible = ref(false)
-// 添加抽屉显示状态
-const drawerVisible = ref(false)
 // 渲染页面标题
 const boxIconTitle = ref('')
+// 定义组件的props
+const props = defineProps({
+  courseData: {
+    type: Object,
+    default: null
+  },
+  courseList: {
+    type: Array,
+    default: () => []
+  }
+})
+
+// 定义emit事件
+const emit = defineEmits(['closeVideo'])
+
 // 课程集合数据
 const courseList = ref([])
-//当前课程
+//当前课程 - 重新定义course来接收传递过来的数据
 const course = ref({})
 // 菜单数据
 const menuItems = ref([])
@@ -231,8 +171,6 @@ const watchedCourseIds = ref([])
 const questionDialogVisible = ref(false)
 // 当前显示的试题
 const courseConfig = ref({})
-// 搜索框
-const SearchInput = ref('')
 // 年级id
 const gradeId = ref('')
 // 课程大纲id
@@ -241,13 +179,10 @@ const typeId = ref('')
 const courseId = ref('')
 // 课程排序
 const typeSort = ref('')
-
 // 课程权限
 const courseDataScope = ref([])
 // 测试账号禁用视频
 const isDisabled = ref(false)
-
-
 //课程小节字典
 const menuDict = ref({
   1: '课前回顾',
@@ -257,35 +192,18 @@ const menuDict = ref({
   5: '课程总结'
 })
 
-// 切换抽屉显示状态的函数
-const toggleDrawer = () => {
-  drawerVisible.value = !drawerVisible.value
-}
-
-// 返回上一页
-const goBack = () => {
-  router.go(-1)
-}
-
-// 菜单打开和关闭的处理函数
-const handleOpen = () => {}
-const handleClose = () => {}
-
-// 菜单选择的处理函数
-const handleSelect = index => {
 
+// 视频切换事件处理函数
+const switchVideo = (index) => {
   // 根据索引切换视频
   if (videoPathMap.value[index]) {
     course.value = videoPathMap.value[index]
     courseId.value = course.value.id
     console.log("课程id:",courseId.value)
-    // 切换标题后,关闭抽屉
-    drawerVisible.value = false
   } else {
     //视频不存在
     Message().notifyWarning('视频不存在!', true)
   }
-
   //测试账号禁用视频
   if (disableVideo(index)) return
 }
@@ -308,21 +226,67 @@ const flattenMenuItems = () => {
 
 // 播放上一个视频
 const playPreviousVideo = () => {
-  const allIndices = flattenMenuItems()
-  const currentIndexInList = allIndices.indexOf(course.value.key)
-  if (currentIndexInList > 0) {
-    const previousIndex = allIndices[currentIndexInList - 1]
-    handleSelect(previousIndex)
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
+    if (currentIndex > 0) {
+      const previousCourse = props.courseList[currentIndex - 1]
+      // 更新当前课程数据
+      course.value = {
+        id: previousCourse.id,
+        courseName: previousCourse.bcName,
+        courseContentType: previousCourse.bcContentType,
+        courseVideoPath: previousCourse.bcContent,
+        courseConfigList: previousCourse.blocklyConfigList,
+        key: previousCourse.id.toString(),
+        // blockly相关属性,用于MapGame组件
+        blocklyBackground: previousCourse.blocklyBackground,
+        blocklyTileSize: previousCourse.blocklyTileSize,
+        blocklyStartPoint: previousCourse.blocklyStartPoint,
+        blocklyEndPoint: previousCourse.blocklyEndPoint,
+        blocklyWalkablePoints: previousCourse.blocklyWalkablePoints,
+        blocklyUserDirection: previousCourse.blocklyUserDirection || 0,
+        blocklyUserImage: previousCourse.blocklyUserImage,
+        blocklyInfo: previousCourse.blocklyInfo
+      };
+      courseId.value = course.value.id;
+      // 更新标题
+      boxIconTitle.value = course.value.courseName;
+      // 禁用视频检查
+      disableVideo(course.value.key);
+    }
   }
 }
 
 // 播放下一个视频
 const playNextVideo = () => {
-  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 (props.courseList && props.courseList.length > 0) {
+    const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
+    if (currentIndex !== -1 && currentIndex < props.courseList.length - 1) {
+      const nextCourse = props.courseList[currentIndex + 1]
+      // 更新当前课程数据
+      course.value = {
+        id: nextCourse.id,
+        courseName: nextCourse.bcName,
+        courseContentType: nextCourse.bcContentType,
+        courseVideoPath: nextCourse.bcContent,
+        courseConfigList: nextCourse.blocklyConfigList,
+        key: nextCourse.id.toString(),
+        // blockly相关属性,用于MapGame组件
+        blocklyBackground: nextCourse.blocklyBackground,
+        blocklyTileSize: nextCourse.blocklyTileSize,
+        blocklyStartPoint: nextCourse.blocklyStartPoint,
+        blocklyEndPoint: nextCourse.blocklyEndPoint,
+        blocklyWalkablePoints: nextCourse.blocklyWalkablePoints,
+        blocklyUserDirection: nextCourse.blocklyUserDirection || 0,
+        blocklyUserImage: nextCourse.blocklyUserImage,
+        blocklyInfo: nextCourse.blocklyInfo
+      };
+      courseId.value = course.value.id;
+      // 更新标题
+      boxIconTitle.value = course.value.courseName;
+      // 禁用视频检查
+      disableVideo(course.value.key);
+    }
   }
 }
 
@@ -348,7 +312,16 @@ const handleVideoEnded = () => {
     const currentIndexInList = allIndices.indexOf(course.value.key)
     if (currentIndexInList !== -1 && currentIndexInList < allIndices.length - 1) {
       const nextIndex = allIndices[currentIndexInList + 1]
-      handleSelect(nextIndex)
+      if (videoPathMap.value[nextIndex]) {
+        course.value = videoPathMap.value[nextIndex]
+        courseId.value = course.value.id
+        console.log("课程id:",courseId.value)
+        //测试账号禁用视频
+        if (disableVideo(nextIndex)) return
+      } else {
+        //视频不存在
+        Message().notifyWarning('视频不存在!', true)
+      }
     }
   }
 
@@ -356,7 +329,16 @@ const handleVideoEnded = () => {
   const currentIndexInList = allIndices.indexOf(course.value.key)
   if (currentIndexInList !== -1 && currentIndexInList < allIndices.length - 1) {
     const nextIndex = allIndices[currentIndexInList + 1]
-    handleSelect(nextIndex)
+    if (videoPathMap.value[nextIndex]) {
+      course.value = videoPathMap.value[nextIndex]
+      courseId.value = course.value.id
+      console.log("课程id:",courseId.value)
+      //测试账号禁用视频
+      if (disableVideo(nextIndex)) return
+    } else {
+      //视频不存在
+      Message().notifyWarning('视频不存在!', true)
+    }
   }
 }
 
@@ -425,33 +407,6 @@ const handleSubmitAnswer = ({ selectedOption }) => {
   questionDialogVisible.value = false
 }
 
-// 搜索
-const querySearch = (queryString, cb) => {
-  const sections = getAllCourseSections()
-  const results = queryString
-    ? sections.filter(section =>
-        section.title.toLowerCase().includes(queryString.toLowerCase())
-      )
-    : sections
-  cb(results)
-}
-
-const filteredTitles = computed(() => {
-  const sections = getAllCourseSections()
-  if (!SearchInput.value) {
-    return sections
-  }
-  return sections.filter(section =>
-    section.title.toLowerCase().includes(SearchInput.value.toLowerCase())
-  )
-})
-
-const handleSearchSelect = item => {
-  handleSelect(item.key)
-  // 清空输入框
-  SearchInput.value = ''
-}
-
 // 课程小节数据提取
 const getAllCourseSections = () => {
   let sections = []
@@ -468,109 +423,168 @@ const getAllCourseSections = () => {
   return sections
 }
 
-// 渲染 课程数据结构 以及 视频
-onMounted(async () => {
+// 初始化课程数据范围
+const initCourseDataScope = () => {
+  const scope = localStorage.getItem("courseDataScope");
+  if (scope) {
+    courseDataScope.value = scope.split(",");
+  }
+};
+
+// 处理父组件传递的课程数据
+const handleParentCourseData = () => {
+  if (!props.courseData) return false;
+  console.log('从父组件接收到的课程数据:', props.courseData);
+  // 设置返回按钮标题
+  boxIconTitle.value = props.courseData.bcName;
+  // 重新定义course接收传递过来的数据
+  course.value = {
+    id: props.courseData.id,
+    courseName: props.courseData.bcName,
+    courseContentType: props.courseData.bcContentType,
+    courseVideoPath: props.courseData.bcContent,
+    courseConfigList: props.courseData.blocklyConfigList,
+    key: props.courseData.id.toString(),
+    // blockly相关属性,用于MapGame组件
+    blocklyBackground: props.courseData.blocklyBackground,
+    blocklyTileSize: props.courseData.blocklyTileSize,
+    blocklyStartPoint: props.courseData.blocklyStartPoint,
+    blocklyEndPoint: props.courseData.blocklyEndPoint,
+    blocklyWalkablePoints: props.courseData.blocklyWalkablePoints,
+    blocklyUserDirection: props.courseData.blocklyUserDirection || 0,
+    blocklyUserImage: props.courseData.blocklyUserImage,
+    blocklyInfo: props.courseData.blocklyInfo
+  };
+  courseId.value = course.value.id;
+  // 如果有配置,禁用视频检查
+  if (!disableVideo(course.value.key)) {
+    console.log('课程已加载:', course.value);
+  }
+  return true;
+};
+
+// 处理课程数据列表
+const processCourseDataList = (data) => {
+  // 对返回的课程数据进行处理,确保ccTime为有效秒数
+  return data.map(course => {
+    // 检查并处理courseConfigList
+    if (course.courseConfigList && Array.isArray(course.courseConfigList)) {
+      // 过滤掉ccTime为0的配置项
+      const validConfigList = course.courseConfigList.filter(config => 
+        config.ccTime !== undefined && config.ccTime !== null && config.ccTime > 0
+      );
+      return {
+        ...course,
+        courseConfigList: validConfigList
+      };
+    }
+    return course;
+  });
+};
+
+// 构建菜单结构
+const buildMenuStructure = () => {
+  let topName = '';
+  courseList.value.forEach((courseTemp, index) => {
+    let menuIndex = courseTemp.courseLabel + '-' + (index + 1);
+    // 创建菜单项
+    let menu = {
+      key: menuIndex,
+      index: menuIndex,
+      title: courseTemp.courseName
+    };
+    if (topName === courseTemp.courseLabel) {
+      // 如果与上一个课程同属一个大节,添加到子菜单
+      let topMenu = menuItems.value[menuItems.value.length - 1];
+      let topMenuChildren = topMenu.children;
+      if (topMenuChildren) {
+        topMenuChildren.push(menu);
+      } else {
+        // 如果当前大节还没有子菜单,创建子菜单结构
+        topMenu = {
+          ...topMenu,
+          title: menuDict.value[courseTemp.courseLabel],
+          children: [
+            {
+              key: topMenu.key,
+              index: topMenu.index,
+              title: topMenu.title,
+            },
+            menu
+          ]
+        };
+        menuItems.value[menuItems.value.length - 1] = topMenu;
+      }
+    } else {
+      // 如果是新的大节,直接添加到菜单
+      menuItems.value.push(menu);
+    }
+    topName = courseTemp.courseLabel;
+    courseTemp['key'] = menuIndex;
+    videoPathMap.value[menuIndex] = courseTemp;
+
+    // 确定默认课程
+    if (index === 0) {
+      courseId.value = courseTemp.id;
+      if (!disableVideo(menuIndex)) {
+        course.value = courseTemp;
+      } else {
+        course.value.key = courseTemp.key;
+        course.value.courseName = courseTemp.courseName;
+      }
+    }
+  });
+};
 
-  if (localStorage.getItem("courseDataScope")) {
-    courseDataScope.value = localStorage.getItem("courseDataScope").split(",");
+// 初始化已观看课程ID
+const initWatchedCourseIds = () => {
+  const savedWatchedIds = localStorage.getItem('watchedCourseIds');
+  if (savedWatchedIds) {
+    try {
+      watchedCourseIds.value = JSON.parse(savedWatchedIds);
+    } catch (error) {
+      console.error('解析已观看课程ID失败:', error);
+      watchedCourseIds.value = [];
+    }
   }
+};
 
-   const typeIdParam = router.currentRoute.value.query.typeId
+// 渲染 课程数据结构 以及 视频
+onMounted(async () => {
+  // 初始化课程数据范围
+  initCourseDataScope();
+  // 检查是否有从父组件传递的courseData
+  if (handleParentCourseData()) {
+    return;
+  }
+  // 从路由参数获取typeId
+  const typeIdParam = router.currentRoute.value.query.typeId;
   if (typeIdParam) {
-    typeId.value = typeIdParam
+    typeId.value = typeIdParam;
     try {
       // 取接口课程数据
-      const res = await ClassType(typeIdParam)
-      // 对返回的课程数据进行处理,确保ccTime为有效秒数
-      const processedData = res.data.map(course => {
-        // 检查并处理courseConfigList
-        if (course.courseConfigList && Array.isArray(course.courseConfigList)) {
-          // 过滤掉ccTime为0的配置项
-          const validConfigList = course.courseConfigList.filter(config => 
-            config.ccTime !== undefined && config.ccTime !== null && config.ccTime > 0
-          )
-          return {
-            ...course,
-            courseConfigList: validConfigList
-          }
-        }
-        return course
-      })
-      courseList.value = processedData
+      const res = await ClassType(typeIdParam);
+      // 处理课程数据
+      courseList.value = processCourseDataList(res.data);
       // 初始化已观看课程ID
-      const savedWatchedIds = localStorage.getItem('watchedCourseIds')
-      if (savedWatchedIds) {
-        watchedCourseIds.value = JSON.parse(savedWatchedIds)
-      }
-      //课程数据
-      let topName = '';
-      courseList.value.forEach((courseTemp, index) => {
-        let menuIndex = courseTemp.courseLabel + '-' + (index + 1)
-        //大节
-        let menu = {
-          key: menuIndex,
-          index: menuIndex,
-          title: courseTemp.courseName
-        }
-
-        if (topName === courseTemp.courseLabel) {
-          let topMenu = menuItems.value[menuItems.value.length - 1]
-          let topMenuChildren = topMenu.children;
-          if (topMenuChildren) {
-            topMenuChildren.push(menu)
-          } else {
-            menu = {
-              key: menuIndex,
-              index: menuIndex,
-              title: menuDict.value[courseTemp.courseLabel],
-              children: [
-                {
-                  key: topMenu.key,
-                  index: topMenu.index,
-                  title: topMenu.title,
-                },
-                {
-                  key: menuIndex,
-                  index: menuIndex,
-                  title: courseTemp.courseName
-                }
-              ]
-            }
-            menuItems.value[menuItems.value.length-1] = menu
-          }
-        }else{
-          menuItems.value.push(menu)
-        }
-        topName = courseTemp.courseLabel
-        courseTemp['key'] = menuIndex
-        videoPathMap.value[menuIndex] = courseTemp
-
-        //确定默认课程
-        if (index === 0) {
-          courseId.value = courseTemp.id
-          if(!disableVideo(menuIndex)){
-            course.value = courseTemp
-          }else{
-            course.value.key = courseTemp.key
-            course.value.courseName = courseTemp.courseName
-          }
-        }
-      })
-
+      initWatchedCourseIds();
+      // 构建菜单结构
+      buildMenuStructure();
+      
     } catch (error) {
-      console.error('获取课程数据失败:', error)
+      console.error('获取课程数据失败:', error);
+      ElMessage.error('获取课程数据失败,请稍后重试');
     }
   }
 
-  const title = router.currentRoute.value.query.typeName
+  // 设置页面标题和排序
+  const title = router.currentRoute.value.query.typeName;
   if (title) {
-    boxIconTitle.value = String(title)
+    boxIconTitle.value = String(title);
   }
-
-  typeSort.value = router.currentRoute.value.query.typeSort
+  typeSort.value = router.currentRoute.value.query.typeSort;
   // 初始化年级ID
-  gradeId.value = globalState.initGradeId()
-
+  gradeId.value = globalState.initGradeId();
 })
 
 onBeforeUnmount(() => {
@@ -635,48 +649,14 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
 }
 
 /* 添加过渡样式 */
-.drawer-slide-enter-active,
-.drawer-slide-leave-active {
-  transition: all 0.3s ease;
-}
-.drawer-slide-enter-from,
-.drawer-slide-leave-to {
-  transform: translateX(-100%);
-  opacity: 0;
-}
-.home-container ::v-deep(.el-drawer__body) {
-  width: rpx(135);
-  height: 100%;
-  position: relative;
-  background: linear-gradient(to bottom, #001169, #8a78d0);
-}
+
 .content-box {
-  flex: 1;
+  width: 100%;
   height: 100%;
   display: flex;
   flex-direction: column; /* 子元素上下排列 */
   background: linear-gradient(to bottom, #001169, #8a78d0);
 }
-.icon-expand {
-  width: rpx(8);
-  height: rpx(35);
-  border-top-right-radius: rpx(5);
-  border-bottom-right-radius: rpx(5);
-  z-index: 9999;
-  position: absolute;
-  top: 50%;
-  transform: translateY(-50%);
-  cursor: pointer; // 添加鼠标指针样式
-  clip-path: polygon(0 0, 100% 15%, 100% 85%, 0 100%);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  transition: all 0.3s ease;
-}
-.icon-expand .vertical-lines {
-  color: #8a78d0;
-  font-size: rpx(10);
-}
 .home-container {
   position: fixed;
   top: 0;
@@ -684,131 +664,6 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
   right: 0;
   bottom: 0;
   background: linear-gradient(to bottom, #001169, #b4a8e1);
-  display: flex;
-}
-.el-row {
-  margin: auto;
-  margin-top: rpx(20);
-}
-.tac ::v-deep(.el-menu) {
-  background-color: transparent;
-  border: none;
-  width: 100%;
-  margin-top: rpx(8);
-}
-
-/* 取消点击后的蓝色字体 el-sub-menu__title*/
-.tac ::v-deep(.el-menu-item.is-active),
-.tac ::v-deep(.el-sub-menu__title.is-active) {
-  color: white;
-}
-// 取消鼠标悬浮颜色
-.tac ::v-deep(.el-sub-menu__title) {
-  width: rpx(130);
-  height: rpx(20);
-  margin-bottom: rpx(5);
-  border-radius: rpx(6);
-  // 添加flex布局使标题和图标两端对齐
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.el-menu ::v-deep(.el-sub-menu__title:hover),
-.el-menu ::v-deep(.el-sub-menu__title:focus),
-.el-menu ::v-deep(.el-sub-menu__title:active) {
-  background: linear-gradient(to bottom, #fee78a, #ffce1b);
-  background-color: transparent;
-}
-// 添加二级标题折叠图标样式
-::v-deep(.el-sub-menu__icon-arrow) {
-  color: white;
-  font-size: rpx(10); // 增大图标尺寸
-  margin-left: auto; // 将图标推到右侧
-  margin-top: rpx(-5);
-  display: block;
-  width: rpx(16); // 确保有明确宽度
-}
-// 鼠标悬停时的图标样式
-.el-menu ::v-deep(.el-sub-menu__title:hover .el-sub-menu__icon-arrow) {
-  color: black; // 与悬停状态的文字颜色保持一致
-}
-.el-menu ::v-deep(.el-icon svg) {
-  font-size: rpx(9);
-}
-
-.mb-2 {
-  color: white;
-  font-size: rpx(9);
-}
-.mb-2 img {
-  width: rpx(10);
-  height: rpx(10);
-  vertical-align: middle;
-  margin-top: rpx(-2);
-}
-.el-menu-item {
-  width: rpx(100);
-  height: rpx(20);
-  margin-bottom: rpx(5);
-  border-radius: rpx(6);
-  color: white;
-  font-size: rpx(8);
-}
-.el-menu ::v-deep(.el-sub-menu__title) {
-  color: white;
-  width: rpx(100);
-  height: rpx(20);
-  margin-bottom: rpx(5);
-  font-size: rpx(8);
-}
-.el-menu ::v-deep(.el-sub-menu__title:hover),
-.el-menu ::v-deep(.el-sub-menu__title:focus),
-.el-menu ::v-deep(.el-sub-menu__title:active) {
-  background: linear-gradient(to bottom, #fee78a, #ffce1b);
-  color: black;
-}
-.el-menu ::v-deep(.el-menu-item:hover),
-.el-menu ::v-deep(.el-menu-item:focus),
-.el-menu ::v-deep(.el-menu-item:active) {
-  background: linear-gradient(to bottom, #fee78a, #ffce1b);
-  color: black;
-  font-size: rpx(8);
-  box-shadow: 0 4px 8px rgba(3, 3, 3, 0.3);
-}
-.el-menu .el-menu-item.is-active {
-  background: linear-gradient(to bottom, #fee78a, #ffce1b);
-  color: black;
-  font-size: rpx(8);
-  box-shadow: 0 4px 8px rgba(3, 3, 3, 0.3);
-}
-.drawer-box {
-  position: absolute;
-  display: flex;
-  align-items: center;
-  height: 100%;
-  width: 80%;
-}
-.drawer-box .toggle-button {
-  width: rpx(10);
-  height: rpx(50);
-  font-size: rpx(7);
-  background-color: rgba(17, 23, 29, 0.2);
-  border: none;
-  position: relative;
-  writing-mode: vertical-lr; // 文字垂直排列,从左到右
-  text-orientation: upright; // 文字保持正立
-}
-.toggle-button:hover {
-  left: 0;
-}
-
-.toggle-button.is-active,
-.toggle-button:active,
-.toggle-button:focus {
-  background-color: rgba(165, 209, 247, 0.8);
-  color: white;
-  border: none; // 移除点击时的边框
-  outline: none; // 移除点击时的外边框
 }
 
 .box-1 {

+ 18 - 18
src/views/programming/ProgrammingCourset.vue

@@ -20,7 +20,7 @@
       <!-- 课程提示 -->
       <div class="right-box">
         <div class="top-right-inner-box">
-          <div class="course-info-box">初始编程</div>
+          <div class="course-info-box">{{ originalCourseTitle }}</div>
         </div>
       </div>
     </div>
@@ -43,11 +43,8 @@
       </div>
     </div>
     
-    <!-- 编程视频界面 -->
-    <ProgrammingVideo v-if="showVideo" />
-    
-    <!-- 编程游戏界面 -->
-    <MapGame v-if="showGame" />
+    <!-- 视频和编程界面 -->
+    <Interface v-if="showVideo" :courseData="selectedCourseData" :courseList="resData" @closeVideo="showVideo = false" />
 
   </div>
 </template>
@@ -64,10 +61,9 @@ import { TopicType } from '@/api/programming/index.js'
 import  explanation  from '@/assets/programming/explanation.png'
 import  practice  from '@/assets/programming/practice.png'
 import  summary  from '@/assets/programming/summary.png'
-// 编程视频界面
-import ProgrammingVideo from '../programming/ProgrammingVideo.vue'
-// 编程游戏界面
-import MapGame from '../block/MapGame.vue'
+
+import Interface from './Interface.vue'
+
 
 // 获取路由实例
 const router = useRouter()
@@ -81,10 +77,12 @@ const originalCourseTitle = ref('')
 const topicId = ref('')
 // 控制视频界面显示
 const showVideo = ref(false) 
- // 控制游戏界面显示
-const showGame = ref(false)
 // 动态课程项数据
 const courseItems = ref([])
+// 当前选中的课程数据
+const selectedCourseData = ref(null)
+// 保存原始API返回的数据
+const resData = ref([])
 
 // 组件挂载时获取路由参数设置标题
 onMounted(() => {
@@ -106,6 +104,8 @@ onMounted(() => {
     TopicType(topicId.value).then(res => {
       console.log(topicId.value, res);
       if (res && res.data && Array.isArray(res.data)) {
+        // 保存原始API返回的数据
+        resData.value = res.data;
         // 创建映射,保持原有的图片和位置样式
         const positionMap = {
           1: { image: explanation, positionClass: 'left-content-box' },
@@ -132,12 +132,13 @@ onMounted(() => {
 
 // 处理课程项点击事件
 const handleCourseItemClick = (item) => {
-  if (item.contentType === 'video') {
+  if (item.contentType === 'video' || item.contentType === 'blockly') {
     showVideo.value = true
-    showGame.value = false
-  } else if (item.contentType === 'blockly') {
-    showVideo.value = false
-    showGame.value = true
+    // 查找并保存完整的课程数据
+    const fullCourseData = resData.value.find(course => course.id === item.id)
+    if (fullCourseData) {
+      selectedCourseData.value = fullCourseData
+    }
   }
 }
 
@@ -145,7 +146,6 @@ const handleCourseItemClick = (item) => {
 const goBackIndex = () => {
   // 隐藏视频和游戏界面
   showVideo.value = false
-  showGame.value = false
   // 返回时携带原始的课程参数,使用categoryId保持与ProgrammingList.vue中参数名一致
   router.push({
     path: '/programminglist',

+ 8 - 1
src/views/programming/ProgrammingGame.vue

@@ -4,7 +4,7 @@
       <!-- 标题部分 -->
       <div class="top-box">
         <div class="top-left-box">
-          <div class="top-left-inner-box">
+          <div class="top-left-inner-box" @click="goToHomePage">
             <!-- 左侧返回图标 -->
              <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
              <span class="left-text">首页</span>
@@ -168,6 +168,13 @@ const goToProgrammingList = (course, index) => {
   })
 }
 
+// 返回到首页
+const goToHomePage = () => {
+  router.push({
+    path: '/home'
+  })
+}
+
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
src/views/programming/ProgrammingList.vue

@@ -87,7 +87,7 @@ const categoryId = ref('')
 
 // 返回上一页
 const goBackIndex = () => {
-  router.push('/programming02')
+  router.push('/programming')
 }
 
 // 定义课程数据