Sfoglia il codice sorgente

AI实验课界面及调取后台数据

丸子 3 mesi fa
parent
commit
bbb99de28e

+ 26 - 0
src/api/laboratory/index.js

@@ -0,0 +1,26 @@
+import axios from '@/utils/request'
+
+// 获取实验室主题
+export function getThemeExperimentList (data) {
+  return axios({
+    url: 'bjdxWeb/aiCourse/getTypeTheme',
+    method: 'get',
+    data
+  })
+}
+
+// 获取实验室类型
+export function getExperimentTypeId (id) {
+  return axios({
+    url: 'bjdxWeb/aiCourse/getTypeByThemeId?id=' + id,
+    method: 'get'
+  })
+}
+
+// 根据ID获取实验室课程列表
+export function getCourseByTypeId (typeId) {
+  return axios({
+    url: 'bjdxWeb/aiCourse/getAiCourseByTypeId?typeId=' + typeId,
+    method: 'get' 
+  })
+}

BIN
src/assets/laboratory/laboratory-bg.png


+ 3 - 13
src/components/HomePage.vue

@@ -32,23 +32,13 @@
             @click="router.push('/programming')"
             >AI编程课</el-button
           >
-                    <el-button
-            round
-            class="top-right-btn"
-            :class="{ 'is-active': selectedButton === 'AI写作课' }"
-            @click="
-              ;(selectedButton = 'AI通识课'),
-                Message().notifyWarning('此版本未开放,敬请期待!', true)
-            "
-            >AI写作课</el-button
-          >
-          <!-- <el-button
+          <el-button
             round
             class="top-right-btn"
             :class="{ 'is-active': selectedButton === 'AI实验课' }"
-            @click="router.push('/experimental-topic')"
+            @click="router.push('/experimental-theme')"
             >AI实验课</el-button
-          > -->
+          >
           <el-button
             round
             class="top-right-btn"

+ 21 - 0
src/router/index.js

@@ -112,6 +112,27 @@ const routes = [
     path: '/interface', 
     component: () => import('../views/programming/Interface.vue')
   },
+  // AI实验室
+  // 实验室主题
+  {
+    path: '/experimental-theme', 
+    component: () => import('../views/laboratory/ExperimentalTheme.vue')
+  },
+  // 实验室类型
+  {
+    path: '/experiment-type', 
+    component: () => import('../views/laboratory/ExperimentType.vue')
+  },
+  // 实验课程
+  {
+    path: '/experimental-course', 
+    component: () => import('../views/laboratory/ExperimentalCourses.vue')
+  },
+  // 实验界面
+  {
+    path: '/experimental-interface', 
+    component: () => import('../views/laboratory/ExperimentalInterface.vue')
+  }
 ]
 const router = createRouter({
   history: createWebHashHistory(),

+ 665 - 0
src/views/laboratory/ExperimentType.vue

@@ -0,0 +1,665 @@
+<!-- 实验室类型 -->
+<template>
+    <div class="programming-content">
+      <!-- 标题部分 -->
+      <div class="top-box">
+        <div class="top-left-box">
+          <div class="top-left-inner-box">
+            <!-- 左侧返回图标 -->
+            <div class="left-content-wrapper" @click="goBackToTopic">
+              <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+              <span class="left-text">AI实验室</span>
+            </div>
+          </div>
+        </div>
+        <div class="top-center-box">
+          <div class="top-center-inner-box">
+            <span>{{ headerTitle }}</span>
+          </div>
+        </div>
+        <div class="top-right-box">
+          <div class="top-right-inner-box"></div>
+        </div>
+      </div>
+
+        <!-- 编程部分 -->
+         <div class="middle-wrapper">
+            <div
+              class="middle-box"
+              ref="middleBox"
+              @mousedown="handleMouseDown"
+              @mousemove="handleMouseMove"
+              @mouseup="handleMouseUp"
+              @mouseleave="handleMouseLeave"
+              @scroll="handleScroll"
+            >
+              <div
+                v-for="(experimentType, index) in experimentTypeList"
+                :key="experimentType.id"
+                class="middle-inner-box"
+                @click="goToProgrammingList(experimentType, index)"
+                v-memo="[activeButton === index, experimentType.isDisabled]"
+              >
+                <div class="new-white-box"
+                     :class="{ 'active': activeButton === index, 'disabled': experimentType.isDisabled }"
+                     :style="{ '--lock-image': `url(${lockImage})` }"
+                     >
+                    <!-- 列表封面图 -->
+                  <div class="bg-image-container" :style="{ backgroundImage: `url(${experimentType.bgImage})` }">
+                    <!-- 星星图标 -->
+                    <template v-if="!experimentType.isDisabled">
+                      <div class="stars-container">
+                        <div
+                          v-for="starIndex in 5"
+                          :key="starIndex"
+                          class="star-icon"
+                          :style="{
+                            backgroundImage: `url(${starIndex <= experimentType.progress ? star01Image : star02Image})`,
+                            '--star-index': starIndex
+                          }"
+                        ></div>
+                      </div>
+                    </template>
+                  </div>
+                  <div class="text-container">
+                    <div class="box-title">
+                      <span>{{ experimentType.title }}</span>
+                    </div>
+                  </div>
+                  <!-- unlock背景图盒子 -->
+                  <div class="unlock-box" :style="{ backgroundImage: experimentType.isDisabled ? `url(${lockImage})` : experimentType.progress > 0 ? `url(${trophyImage})` : `url(${unlockImage})` }"></div>
+                </div>
+              </div>
+            </div>
+         </div>
+
+        <!-- 底部切换按钮 -->
+        <div class="bottom-box">
+          <!-- 左切换按钮 -->
+          <div class="carousel-btn prev-btn" @click="prevExperimentType" :class="{ 'disabled-btn': activeButton === 0 }" :disabled="activeButton === 0">
+            <img :src="leftbtn" alt="左按钮" class="btn-image" :class="{ 'disabled-icon': activeButton === 0 }" />
+          </div>
+          <!-- 进度条滑块 -->
+          <div class="line-container" v-if="circleButtons.length > 0">
+            <el-slider v-model="activeButton" :max="circleButtons.length - 1" :step="1" :show-tooltip="false" />
+          </div>
+          <!-- 右切换按钮 -->
+          <div class="carousel-btn next-btn" @click="nextExperimentType" :class="{ 'disabled-btn': activeButton >= experimentTypeList.length - 1 }" :disabled="activeButton >= experimentTypeList.length - 1">
+            <img :src="rightbtn" alt="右按钮" class="btn-image" :class="{ 'disabled-icon': activeButton >= experimentTypeList.length - 1 }" />
+          </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue'
+// 返回图标
+import { ArrowLeftBold } from '@element-plus/icons-vue';
+// 导入按钮图片
+import leftbtn from '@/assets/programming/leftbtn.png'
+import rightbtn from '@/assets/programming/rightbtn.png'
+// 导入路由
+import { useRouter } from 'vue-router';
+// 导入图片
+import lockImage from '@/assets/programming/lock.png'
+import unlockImage from '@/assets/programming/unlock.png'
+import trophyImage from '@/assets/programming/trophy.png'
+// 星星图片
+import star02Image from '@/assets/programming/star02.png'
+import star01Image from '@/assets/programming/star01.png'
+import {Message} from "@/utils/message/Message.js";
+
+// 获取实验室类型接口
+import { getExperimentTypeId } from '@/api/laboratory/index.js'
+
+// 常量配置
+const CONSTANTS = {
+  SCROLL_SPEED: 2, // 滚动速度
+  ANIMATION_DURATION: '0.3s',
+  DEFAULT_ACTIVE_INDEX: 0 // 索引
+}
+
+// 获取路由实例
+const router = useRouter()
+// 页面标题
+const headerTitle = ref('')
+// 主题ID
+const themeId = ref('')
+// 拖动相关变量
+const isDragging = ref(false)
+const startX = ref(0)
+const scrollLeft = ref(0)
+
+// 返回上一页
+const goBackToTopic = () => {
+  router.push('/experimental-theme')
+}
+
+// 定义实验类型数据
+const experimentTypeList = reactive([])
+
+// 当前激活的按钮索引
+const activeButton = ref(CONSTANTS.DEFAULT_ACTIVE_INDEX)
+
+// 计算属性:根据实验类型列表生成圆形按钮数据
+const circleButtons = computed(() => {
+  return experimentTypeList.map((_, index) => ({ 
+    text: String(index + 1) 
+  }))
+})
+
+// 获取middleBox元素的引用
+const middleBox = ref(null)
+
+// 自动滚动到中间位置
+watch(activeButton, (newIndex) => {
+  if (middleBox.value && newIndex !== -1) {
+    // 找到对应的课程卡片元素
+    const courseElement = middleBox.value.querySelector(`.middle-inner-box:nth-child(${newIndex + 1})`);
+    if (courseElement) {
+      // 计算滚动位置,选中的卡片居中
+      const containerWidth = middleBox.value.clientWidth;
+      const elementLeft = courseElement.offsetLeft;
+      const elementWidth = courseElement.offsetWidth;
+      // 计算居中位置
+      const scrollPosition = elementLeft - (containerWidth / 2) + (elementWidth / 2);
+      // 平滑滚动到计算的位置
+      middleBox.value.scrollTo({
+        left: scrollPosition,
+        behavior: 'smooth'
+      });
+    }
+  }
+});
+
+// 鼠标按下事件处理函数
+const handleMouseDown = (e) => {
+  isDragging.value = true
+  startX.value = e.pageX - middleBox.value.offsetLeft
+  scrollLeft.value = middleBox.value.scrollLeft
+}
+
+// 鼠标移动事件处理函数
+const handleMouseMove = (e) => {
+  if (!isDragging.value) return
+  e.preventDefault()
+  const x = e.pageX - middleBox.value.offsetLeft
+  const walk = (x - startX.value) * CONSTANTS.SCROLL_SPEED // 滚动速度
+  middleBox.value.scrollLeft = scrollLeft.value - walk
+}
+
+// 鼠标松开事件处理函数
+const handleMouseUp = () => {
+  isDragging.value = false
+}
+
+// 鼠标离开事件处理函数
+const handleMouseLeave = () => {
+  isDragging.value = false
+}
+
+// 滚动事件处理函数
+const handleScroll = () => {}
+
+// 上一条数据
+const prevExperimentType = () => {
+  if (activeButton.value > 0) {
+    activeButton.value -= 1;
+  }
+}
+
+// 下一条数据
+const nextExperimentType = () => {
+  if (activeButton.value < experimentTypeList.length - 1) {
+    activeButton.value += 1;
+  }
+}
+
+// 在获取到themeId后再调用获取类型列表getExperimentTypeId接口
+const fetchExperimentTypeList = async () => {
+  if (themeId.value) {
+    try {
+      const res = await getExperimentTypeId(themeId.value)
+      // 更新实验类型数据
+      if (res && res.data && Array.isArray(res.data)) {
+        // 清空原有数据
+        experimentTypeList.splice(0, experimentTypeList.length);
+        res.data.forEach(item => {
+          experimentTypeList.push({
+            id: item.id,
+            title: item.ctType,
+            bgImage: item.ctTypeImage,
+            progress: item.progress,
+            isDisabled: item.dataReadonly
+          });
+        });
+        // 重置激活按钮索引,默认选中第一项
+        activeButton.value = CONSTANTS.DEFAULT_ACTIVE_INDEX;
+      }
+    } catch (error) {
+      console.error('Failed to fetch experiment type list:', error)
+      Message().notifyError('获取实验类型列表失败,请重试', true)
+    }
+  }
+}
+
+// 组件挂载时获取路由参数设置标题和课程ID
+onMounted(() => {
+  // history.state中获取参数
+  const title = history.state?.themeTitle
+  if (title) {
+    headerTitle.value = title
+  }
+  const id = history.state?.themeId
+  if (id) {
+    themeId.value = id
+    // 获取到themeId后,调用getExperimentTypeId接口
+    fetchExperimentTypeList()
+  }
+})
+
+// 组件卸载时清理
+onUnmounted(() => {
+  // 清理事件监听器
+  isDragging.value = false
+})
+
+// 处理项目点击事件-跳转到课程详情页面
+const goToProgrammingList = (experimentType, index) => {
+  // 检查是否禁用
+  if (experimentType.isDisabled) {
+    Message().notifyWarning('您的账号并未开放此课程!', true)
+    return
+  }
+  // 设置当前选中项
+  activeButton.value = index;
+  // 跳转ExperimentalCourses页面,并传递课程信息作为参数
+  router.push({
+    path: '/experimental-course',
+    state: {
+      typeTitle: experimentType.title,
+      typeIndex: index,
+      typeId: experimentType.id, // 当前类型的id
+      originalThemeId: themeId.value, // 主题ID
+      originalThemeTitle: headerTitle.value, // 主题标题
+      isDisabled: experimentType.isDisabled
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.programming-content{
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-image: url('@/assets/laboratory/laboratory-bg.png');
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  display: flex;
+  flex-direction: column;
+}
+.top-box {
+  height: 20%;
+  display: flex;
+}
+.top-left-box,
+.top-right-box {
+  flex: 1;
+  height: 50%;
+  border-radius: rpx(5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.top-center-box {
+  flex: 2;
+  border-radius: rpx(5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.top-left-inner-box,
+.top-center-inner-box,
+.top-right-inner-box {
+  width: 100%;
+  height: 100%;
+}
+.top-center-inner-box{
+  background-image: url('@/assets/programming/list_title.png');
+    background-size: 70%; 
+    background-repeat: no-repeat;
+    background-position: center center; /* 背景图垂直水平居中 */
+    display: flex;
+    align-items: center; /* 垂直居中 */
+    justify-content: center; /* 水平居中 */
+    cursor: pointer;
+    position: relative; /* 相对定位 */
+    span{
+        font-size: rpx(16);
+        color: white;
+        position: relative;
+        z-index: 1; /* 确保文字在背景图上方 */
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding-bottom: rpx(5);
+    }
+  }
+.top-left-inner-box{
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  .left-content-wrapper{
+    display: flex;
+    align-items: center; /* 保持内部元素垂直居中 */
+  }
+  .left-icon{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(20);
+    cursor: pointer;
+  }
+  .left-text{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(10);
+    cursor: pointer;
+  }
+}
+
+.middle-wrapper {
+  height: 75%;
+  overflow: hidden; /* 溢出隐藏 */
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.middle-box {
+  min-width: 100%; /* 固定最小宽度 */
+  height: 100%;
+  overflow-x: auto; /* 水平滚动条 */
+  overflow-y: hidden; /* 取消上下滚动 */
+  white-space: nowrap; /* 防止元素换行 */
+  scroll-behavior: smooth; /* 平滑滚动效果 */
+  -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
+  position: relative;
+  flex: 1;
+}
+
+.middle-inner-box{
+  width: rpx(140); /* 设置固定宽度 */
+  height: 100%; /* 设置固定高度 */
+  position: relative;
+  cursor: pointer;
+  user-select: none; /* 禁止文本选择 */
+  display: inline-flex;
+  align-items: center; /* 垂直居中 */
+  justify-content: center; /* 水平居中 */
+  margin-right: rpx(20);
+  margin-left: rpx(20);
+  margin-top: rpx(-15);
+  vertical-align: middle; /* 垂直居中对齐 */
+}
+
+/* 鼠标按下时的光标样式 */
+.middle-inner-box:active {
+  cursor: grabbing;
+}
+
+/* 隐藏滚动条但保持滚动功能 */
+.middle-box::-webkit-scrollbar {
+  display: none;
+}
+
+.middle-box {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+
+.new-white-box {
+  width: 100%;
+  height: rpx(150);
+  background-color: white;
+  border-radius: rpx(15);
+  display: flex;
+  flex-direction: column;
+  transition: all 0.3s ease; /* 过渡效果 */
+  position: relative;
+}
+
+/* 小三角效果 */
+.new-white-box::after {
+  content: '';
+  position: absolute;
+  bottom: -rpx(8);
+  left: 50.5%;
+  transform: translateX(-50%);
+  width: 0;
+  height: 0;
+  top: rpx(150);
+  border-left: rpx(10) solid transparent;
+  border-right: rpx(10) solid transparent;
+  border-top: rpx(10) solid white;
+}
+
+/* 鼠标悬浮/选中时的放大效果 */
+.new-white-box:hover:not(.disabled),
+.new-white-box.active:not(.disabled) {
+  transform: scale(1.1); /* 放大1.1倍 */
+  z-index: 10; /* 确保放大时在顶层显示 */
+  border: rpx(5) solid #D0D7F7; /* 边框效果 */
+  box-shadow: 0 0 rpx(10) rgba(0, 0, 0, 0.5);
+}
+/* 禁用状态样式 */
+.new-white-box.disabled {
+  cursor: not-allowed;
+  filter: grayscale(0%);
+  pointer-events: none;
+  position: relative;
+}
+
+/* 禁用状态下的文字颜色 */
+.new-white-box.disabled .box-title {
+  color: #676666;
+}
+
+/* 禁用状态下的遮挡层 */
+.new-white-box.disabled::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.3);
+  background-image: var(--lock-image);
+  background-size: 40%;
+  background-position: center;
+  background-repeat: no-repeat;
+  border-radius: rpx(15);
+  z-index: 2;
+}
+/* 小三角效果 */
+.new-white-box.disabled::after {
+  top: rpx(150);
+  opacity: 0.5;
+}
+
+/* 背景图容器 */
+.bg-image-container {
+  width: rpx(140);
+  height: rpx(150);
+  margin-top: rpx(5);
+  background-size: 100%; 
+  background-position: center;
+  background-repeat: no-repeat;
+  position: relative;
+}
+
+/* 星星容器样式 */
+.stars-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: rpx(40);
+  z-index: 1;
+}
+
+/* star图标样式 */
+.star-icon {
+  position: absolute;
+  top: rpx(10);
+  width: rpx(20);
+  height: rpx(20);
+  background-size: contain;
+  background-position: center;
+  background-repeat: no-repeat;
+  z-index: 1;
+  transform-origin: left;
+  transform: scale(1);
+  // 星星间距
+  left: calc(rpx(15) + (var(--star-index) - 1) * rpx(15));
+}
+
+/* 文字容器 */
+.text-container {
+  width: 100%;
+  height: 15%; 
+  display: flex;
+  line-height: rpx(10);
+  justify-content: center;
+}
+
+/* 标题样式 */
+.box-title {
+  color: #333;
+  text-align: center;
+  font-size: rpx(11); 
+  color: #45300b;
+}
+
+/* unlock盒子样式 */
+.unlock-box {
+  position: absolute;
+  width: 100%;
+  height: 30%;
+  background-size: contain;
+  background-position: center;
+  background-repeat: no-repeat;
+  left: 51%;
+  transform: translateX(-50%);
+  top: rpx(148);
+  z-index: 5;
+}
+.bottom-box {
+  height: 15%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.line-container {
+  width: 70%; /* 滑块宽度 */
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  /* el-slider滑块样式 */
+  :deep(.el-slider__runway) {
+    height: rpx(10); /* 滑块轨道高度 */
+    background-color: rgba(228, 227, 254,0.3); /* 轨道背景色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__bar) {
+    height: rpx(10); /* 滑块激活部分高度 */
+    background-color: #2598c2; /* 激活部分颜色为蓝色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__button) {
+    width: rpx(35); 
+    height: rpx(35); 
+    margin-top: rpx(-5);
+    margin-left: rpx(-10);
+    border: none; /* 移除边框 */
+    background-image: url('@/assets/programming/xiaozhi.png'); 
+    background-size: 100%; /* 背景图片完全覆盖按钮 */
+    background-position: center; /* 背景图片居中 */
+    background-repeat: no-repeat; /* 背景图片不重复 */
+    background-color: transparent; /* 透明背景 */
+    &:hover {
+      transform: scale(1.1); /* 悬停时放大效果 */
+    }
+    &.hover,
+    &:active {
+      transform: scale(1.1); /* 激活时放大效果 */
+      box-shadow: none; /* 移除阴影效果 */
+    }
+  }
+  
+  :deep(.el-slider__stop) {
+    width: rpx(10); /* 停止点大小 */
+    height: rpx(10); /* 停止点大小 */
+    background-color: white; /* 停止点颜色 */
+    opacity: 0.2;
+  }
+}
+
+
+
+/* 轮播图按钮样式 */
+.carousel-btn {
+  width: rpx(40);
+  height: rpx(40);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  transition: all 0.3s ease;
+}
+
+.carousel-btn:hover:not(.disabled-btn) {
+  transform: scale(1.1);
+}
+
+.carousel-btn:disabled,
+.disabled-btn {
+  cursor: not-allowed;
+  box-shadow: none;
+}
+
+.disabled-icon {
+  opacity: 0.5;
+}
+
+.prev-btn {
+  margin-right: rpx(5);
+}
+
+.next-btn {
+  margin-left: rpx(5);
+}
+
+.btn-image {
+  width: rpx(40);
+  height: rpx(40);
+  object-fit: contain;
+}
+
+</style>

+ 724 - 0
src/views/laboratory/ExperimentalCourses.vue

@@ -0,0 +1,724 @@
+<!-- 实验课程 -->
+<template>
+  <div class="programming-content">
+    <!-- 标题部分 -->
+    <div class="top-box">
+      <!-- 返回按钮 -->
+      <div class="top-left-box">
+        <div class="top-left-inner-box">
+          <!-- 左侧返回图标 -->
+          <div class="left-content-wrapper" @click="goBackIndex">
+            <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+            <span class="left-text">返回</span>
+          </div>
+        </div>
+      </div>
+      <!-- 标题 -->
+    <div class="top-center-box" v-if="!showVideo">
+      <div class="top-center-inner-box">
+        <span>{{ coursesTitle }}</span>
+      </div>
+    </div>
+      <!-- 课程提示 -->
+      <div class="top-right-box">
+        <div class="top-right-inner-box">
+          <div class="course-info-box">{{ originalCourseTitle }}</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 课程部分 -->
+    <div class="lower-box" v-if="!showVideo">
+        <div 
+            class="content-box"
+            ref="contentBox"
+            @mousedown="handleMouseDown"
+            @mousemove="handleMouseMove"
+            @mouseup="handleMouseUp"
+            @mouseleave="handleMouseLeave"
+            @scroll="handleScroll"
+          >
+          <!-- 动态渲染课程内容 -->
+          <div 
+            v-for="(item, index) in courseList" 
+            :key="item.id"
+            :class="['slide-item', { active: activeButton === index }]"
+            :style="courseList.length <= 3 ? { float: 'left' } : {}"
+            @click="!isDragging && !hasMoved && handleCourseItemClick(item)"
+            v-memo="[activeButton === index, item.isDisabled]"
+          >
+            <div class="box-content">
+              <img :src="item.image" :alt="item.title" class="box-image" />
+              <div class="box-text">{{ item.title }}</div>
+            </div>
+            <!-- 星星图标组 -->
+            <div class="star-group">
+              <div
+                v-for="i in getStarCount(item.contentType)"
+                :key="i"
+                class="star-icon"
+                :style="{
+                  backgroundImage: `url(${i <= item.progress ? star01Image : star02Image})`,
+                }"
+              ></div>
+            </div>
+          </div>
+        </div>
+    </div>
+
+    <!-- 底部切换按钮 -->
+    <div v-if="!showVideo && courseList.length > 3" class="bottom-box">
+      <!-- 左切换按钮 -->
+      <div class="carousel-btn prev-btn" @click="prevExperimentalCourses" :class="{ 'disabled-btn': activeButton === 0 }" :disabled="activeButton === 0">
+        <img :src="leftbtn" alt="左按钮" class="btn-image" :class="{ 'disabled-icon': activeButton === 0 }" />
+      </div>
+      <!-- 进度条滑块 -->
+      <div class="line-container" v-if="courseList.length > 0">
+        <el-slider v-model="activeButton" :max="courseList.length - 1" :step="1" :show-tooltip="false" />
+      </div>
+      <!-- 右切换按钮 -->
+      <div class="carousel-btn next-btn" @click="nextExperimentalCourses" :class="{ 'disabled-btn': activeButton >= courseList.length - 1 }" :disabled="activeButton >= courseList.length - 1">
+        <img :src="rightbtn" alt="右按钮" class="btn-image" :class="{ 'disabled-icon': activeButton >= courseList.length - 1 }" />
+      </div>
+    </div>
+    
+    <!-- 视频和编程界面 -->
+    <ExperimentalInterface v-if="showVideo" :courseData="selectedCourseData" :courseList="resData" @closeVideo="showVideo = false" />
+
+  </div>
+</template>
+
+<script setup>
+// 返回图标
+import { ArrowLeftBold } from '@element-plus/icons-vue';
+import { ref, onMounted, onUnmounted, watch } from 'vue';
+// 导入路由
+import { useRouter } from 'vue-router';
+// 导入图片
+import  explanation  from '@/assets/programming/explanation.png'
+import  practice  from '@/assets/programming/practice.png'
+import  summary  from '@/assets/programming/summary.png'
+// 导入按钮图片
+import leftbtn from '@/assets/programming/leftbtn.png'
+import rightbtn from '@/assets/programming/rightbtn.png'
+import ExperimentalInterface from './ExperimentalInterface.vue'
+// 星星图片
+import star02Image from '@/assets/programming/star02.png'
+import star01Image from '@/assets/programming/star01.png'
+import {Message} from "@/utils/message/Message.js";
+// 根据ID获取实验室课程列表接口
+import { getCourseByTypeId } from '@/api/laboratory/index.js'
+
+// 常量配置
+const CONSTANTS = {
+  SCROLL_SPEED: 2,
+  ANIMATION_DURATION: '0.3s',
+  DEFAULT_ACTIVE_INDEX: 0,
+  // 根据acLabel获取图片
+  IMAGE_MAP: { 
+    '1': explanation,
+    '2': practice,
+    '3': summary
+  }
+}
+
+// 获取路由实例
+const router = useRouter()
+// 页面标题
+const coursesTitle = ref('')
+// 保存原始的课程ID和标题
+const originalCourseId = ref('')
+const originalCourseTitle = ref('')
+const isDisabled = ref(false) // 课程是否只读
+// 类型ID(从ExperimentType页面传递过来)
+const typeId = ref('')
+// 控制视频界面显示
+const showVideo = ref(false) 
+// 动态课程项数据
+const courseList = ref([])
+// 当前选中的课程数据
+const selectedCourseData = ref(null)
+// 保存原始API返回的数据
+const resData = ref([])
+// 当前激活的按钮索引
+const activeButton = ref(CONSTANTS.DEFAULT_ACTIVE_INDEX)
+// 拖动相关变量
+const isDragging = ref(false)
+const startX = ref(0)
+const scrollLeft = ref(0)
+const hasMoved = ref(false) // 标记是否发生了移动
+// 获取contentBox元素的引用
+const contentBox = ref(null)
+
+// 根据内容类型获取星星数量
+const getStarCount = (contentType) => {
+  if (contentType === 'video') {
+    return 1; // video类型渲染1个星星
+  } else if (contentType === 'aiTextToText') {
+    return 3; // aiTextToText类型渲染3个星星
+  }
+  return 0; // 默认返回0个星星
+}
+
+// 上一节课程
+const prevExperimentalCourses = () => {
+  if (activeButton.value > 0) {
+    activeButton.value -= 1;
+  }
+}
+
+// 下一节课程
+const nextExperimentalCourses = () => {
+  if (activeButton.value < courseList.value.length - 1) {
+    activeButton.value += 1;
+  }
+}
+
+// 鼠标按下事件处理函数
+const handleMouseDown = (e) => {
+  // 拖拽状态为true
+  isDragging.value = true
+  // 初始化移动状态为false
+  hasMoved.value = false
+  // 计算鼠标在容器内的相对X坐标的位置  e.pageX鼠标相对于整个页面的X坐标
+  if (contentBox.value) {
+    startX.value = e.pageX - contentBox.value.offsetLeft
+    scrollLeft.value = contentBox.value.scrollLeft
+  }
+}
+
+// 鼠标移动事件处理函数
+const handleMouseMove = (e) => {
+  if (!isDragging.value || !contentBox.value) return
+  // 标记已发生移动
+  hasMoved.value = true
+  // 计算新的滚动位置
+  const x = e.pageX - contentBox.value.offsetLeft
+  const walk = (x - startX.value) * CONSTANTS.SCROLL_SPEED // 滚动速度
+  contentBox.value.scrollLeft = scrollLeft.value - walk
+}
+
+// 鼠标松开事件处理函数
+const handleMouseUp = () => {
+  isDragging.value = false;
+  hasMoved.value = false;
+};
+
+// 鼠标离开事件处理函数
+const handleMouseLeave = () => {
+  isDragging.value = false;
+  hasMoved.value = false;
+};
+
+// 滚动事件处理函数
+const handleScroll = () => {}
+
+// 监听activeButton变化,自动滚动到中间位置
+watch(activeButton, (newIndex) => {
+  if (contentBox.value && newIndex !== -1) {
+    // 使用requestAnimationFrame优化滚动
+    requestAnimationFrame(() => {
+      // 找到对应的课程卡片元素
+      const courseElement = contentBox.value.querySelector(`.slide-item:nth-child(${newIndex + 1})`);
+      if (courseElement) {
+        // 计算滚动位置,选中的卡片居中
+        const containerWidth = contentBox.value.clientWidth;
+        const elementLeft = courseElement.offsetLeft;
+        const elementWidth = courseElement.offsetWidth;
+        // 计算居中位置
+        const scrollPosition = elementLeft - (containerWidth / 2) + (elementWidth / 2);
+        // 平滑滚动到计算的位置
+        contentBox.value.scrollTo({
+          left: scrollPosition,
+          behavior: 'smooth'
+        });
+      }
+    });
+  }
+});
+
+// 获取课程数据函数优化
+const fetchCourseData = async () => {
+  if (typeId.value) {
+    try {
+      const res = await getCourseByTypeId(typeId.value)
+      if (res && res.data && Array.isArray(res.data)) {
+        // 使用局部变量处理数据,减少响应式对象的频繁修改
+        const newResData = [...res.data];
+        newResData.forEach(item => {
+          item.isDisabled = isDisabled.value
+        });
+
+        // 批量创建课程项
+        const newCourseItems = newResData.map((item, index) => {
+          const image = CONSTANTS.IMAGE_MAP[item.acLabel]; // 根据acLabel获取图片
+          return {
+            id: item.id,
+            title: item.acName,
+            image: image,
+            contentType: item.acContentType,
+            progress: item.progress, // 进度
+            isDisabled: isDisabled.value // 保存acLabel用于图片映射
+          };
+        });
+        
+        // 一次性更新响应式数据,减少DOM更新次数
+        resData.value = newResData;
+        courseList.value = newCourseItems;
+      }
+    } catch (error) {
+      console.error('Failed to fetch course data:', error)
+      Message().notifyError('获取课程数据失败,请重试', true)
+    }
+  }
+}
+
+// 组件挂载时获取路由参数设置标题
+onMounted(() => {
+  // 从history.state中获取参数
+  const courseTitle = history.state?.typeTitle;
+  if (courseTitle) {
+    coursesTitle.value = courseTitle;
+  }
+  
+  // 一次性设置多个响应式数据
+  typeId.value = history.state?.typeId;
+  originalCourseId.value = history.state?.originalThemeId;
+  originalCourseTitle.value = history.state?.originalThemeTitle;
+  isDisabled.value = Boolean(history.state?.isDisabled);
+
+  // 调用函数获取课程列表
+  fetchCourseData();
+});
+
+// 组件卸载时清理
+onUnmounted(() => {
+  // 清理拖拽状态
+  isDragging.value = false;
+  hasMoved.value = false;
+});
+
+// 处理课程项点击事件
+const handleCourseItemClick = (item) => {
+  // 如果在拖动过程中或移动过,则不触发点击事件
+  if (hasMoved.value || isDragging.value) return
+
+  // 如果是只读模式,不允许点击
+  if (isDisabled.value) {
+    Message().notifyWarning('您的账号并未开放此课程!', true)
+    return;
+  }
+
+  // 直接使用item中的信息,避免再次查找
+  if (item.contentType === 'video' || item.contentType === 'aiTextToText') {
+    showVideo.value = true
+    selectedCourseData.value = {
+      ...resData.value.find(course => course.id === item.id),
+      ztId: originalCourseId.value,
+      isDisabled: isDisabled.value
+    }
+  }
+}
+
+// 返回编程课列表
+const goBackIndex = () => {
+  // 隐藏视频和游戏界面
+  showVideo.value = false
+  // 返回时携带原始的课程参数
+  router.push({
+    path: '/experiment-type',
+    state: {
+      themeId: originalCourseId.value,
+      themeTitle: originalCourseTitle.value
+    }
+  })
+}
+
+// 监听showVideo状态变化,当从true变为false时重新获取课程数据
+watch(showVideo, (newValue, oldValue) => {
+  // 使用nextTick确保DOM更新完成后再执行
+  if (oldValue === true && newValue === false) {
+    setTimeout(() => {
+      fetchCourseData();
+    }, 0)
+  }
+})
+
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.programming-content{
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-image: url('@/assets/laboratory/laboratory-bg.png');
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  display: flex;
+  flex-direction: column;
+  user-select: none; /* 禁止文本选择 */
+}
+
+.top-box {
+  height: 20%;
+  display: flex;
+  
+}
+
+.top-left-box,
+.top-right-box {
+  flex: 1;
+  height: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.top-center-box {
+  flex: 2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.top-center-inner-box{
+  width: 100%;
+  height: 100%;
+  background-image: url('@/assets/programming/list_title.png');
+  background-size: 70%; 
+    background-repeat: no-repeat;
+    background-position: center center; /* 背景图垂直水平居中 */
+    display: flex; 
+    align-items: center; /* 垂直居中 */
+    justify-content: center; /* 水平居中 */
+    cursor: pointer;
+    position: relative; /* 相对定位 */
+   span{
+        font-size: rpx(16);
+        color: white;
+        position: relative;
+        z-index: 1; /* 确保文字在背景图上方 */
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding-bottom: rpx(5);
+    }
+  }
+
+.top-left-inner-box{
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  width: 100%;
+  height: 100%;
+  .left-content-wrapper{
+    display: flex;
+    align-items: center; /* 保持内部元素垂直居中 */
+  }
+  .left-icon{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(20);
+    cursor: pointer;
+  }
+  .left-text{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(10);
+    cursor: pointer;
+  }
+}
+
+.top-right-inner-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding-right: rpx(20);
+  .course-info-box {
+    width: rpx(60);
+    height: rpx(20);
+    background-color: rgb(255, 255, 255);
+    border-radius: rpx(15);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #45300b;
+    font-size: rpx(10);
+    cursor: pointer;
+  }
+}
+
+.lower-box {
+  height: 75%;
+  overflow: hidden; /* 溢出隐藏 */
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: rpx(-25);
+}
+
+.content-box {
+  width: 100%;
+  min-width: rpx(660); /* 最小宽度 */
+  height: 100%;
+  overflow-x: auto; /* 水平滚动条 */
+  overflow-y: hidden; /* 取消上下滚动 */
+  white-space: nowrap; /* 防止元素换行 */
+  scroll-behavior: smooth; /* 平滑滚动效果 */
+  -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
+  position: relative;
+  flex: 1;
+  cursor: grab; /* 显示可抓取图标 */
+  z-index: 2;
+  padding: 0 rpx(30);
+  /* 设置背景图  */
+  background-image: url('@/assets/programming/track01.png');
+  background-size: rpx(1360) rpx(350); /* 固定宽度,背景图大小一致 */
+  background-position: left calc(-1 * rpx(50));
+  background-repeat: no-repeat;
+  background-attachment: local; /* 背景图跟内容一起滚动 */
+}
+
+/* 鼠标按下时的光标样式 */
+.content-box:active {
+  cursor: grabbing;
+}
+
+/* 隐藏滚动条但保持滚动功能 */
+.content-box::-webkit-scrollbar {
+  display: none;
+}
+
+.content-box {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+.slide-item {
+  width: rpx(130); /* 设置固定宽度 */
+  height: rpx(110); /* 高度设置 */
+  margin: rpx(85) rpx(40);
+  border-radius: rpx(35);
+  background-color: rgba(255, 255, 255);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 0.3s ease;
+  cursor: pointer;
+  z-index: 2; /* 内容在背景图上方 */
+  vertical-align: middle;
+  position: relative;
+}
+/* 奇数项在上层 */
+.slide-item:nth-child(odd) {
+  transform: translateY(-50%);
+}
+/* 偶数项在下层 */
+.slide-item:nth-child(even) {
+  transform: translateY(50%);
+}
+/* 鼠标悬停放大效果 */
+  .slide-item:nth-child(odd):hover {
+    transform: translateY(-50%) scale(1.05); /* 减小放大比例 */
+    z-index: 10;
+    transition: transform 0.2s ease-out; /* 更短更平滑的过渡 */
+  }
+
+  .slide-item:nth-child(even):hover {
+    transform: translateY(50%) scale(1.05); /* 减小放大比例 */
+    z-index: 10;
+    transition: transform 0.2s ease-out; /* 更短更平滑的过渡 */
+  }
+  /* 默认选中第一个元素的样式 */
+  /* 奇数项选中样式 */
+  .slide-item:nth-child(odd).active {
+    transform: translateY(-50%) scale(1.05); /* 减小放大比例 */
+    z-index: 10;
+    box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
+  }
+
+  /* 偶数项选中样式 */
+  .slide-item:nth-child(even).active {
+    transform: translateY(50%) scale(1.05); /* 减小放大比例 */
+    z-index: 10;
+    box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
+  }
+
+/* 内容样式 */
+.box-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  width: 100%;
+}
+
+
+
+/* 鼠标按下时的光标样式 */
+.slide-item:active {
+  cursor: grabbing;
+}
+.box-image {
+  width: 100%;
+  height: 90%;
+  object-fit: contain;
+}
+
+.box-text {
+  width: 100%;
+  height: 20%;
+  line-height: 20%;
+  font-size: rpx(13);
+  color: #333;
+  text-align: center;
+}
+
+/* 星星图标样式 */
+.star-group {
+  position: absolute;
+  top: 100%;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 3;
+}
+
+.star-icon {
+  position: relative;
+  width: rpx(23);
+  height: rpx(23);
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: center;
+  margin: 0 rpx(0);
+}
+
+/* 轮播图按钮样式 */
+.carousel-btn {
+  width: rpx(40);
+  height: rpx(40);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  transition: all 0.3s ease;
+}
+
+.carousel-btn:hover:not(.disabled-btn) {
+ transform: scale(1.1);
+}
+
+.carousel-btn:disabled,
+.disabled-btn {
+  cursor: not-allowed;
+  box-shadow: none;
+}
+
+.disabled-icon {
+  opacity: 0.5;
+}
+
+.prev-btn {
+  margin-right: rpx(5);
+}
+
+.next-btn {
+  margin-left: rpx(5);
+}
+
+.btn-icon {
+  font-size: rpx(15);
+  color: #333;
+}
+
+/* 底部切换按钮样式 */
+.bottom-box {
+  height: 15%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.bottom-box .carousel-btn {
+  position: relative;
+  top: 0;
+  transform: none;
+  width: rpx(40);
+  height: rpx(40);
+  background-color: transparent;
+  box-shadow: none;
+}
+
+.bottom-box .carousel-btn:hover:not(.disabled-btn) {
+  transform: none;
+}
+
+.line-container {
+  width: 70%; /* 滑块宽度 */
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  /* el-slider滑块样式 */
+  :deep(.el-slider__runway) {
+    height: rpx(10); /* 滑块轨道高度 */
+    background-color: rgba(228, 227, 254,0.3); /* 轨道背景色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__bar) {
+    height: rpx(10); /* 滑块激活部分高度 */
+    background-color: #2598c2; /* 激活部分颜色为蓝色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__button) {
+    width: rpx(35); 
+    height: rpx(35); 
+    margin-top: rpx(-5);
+    margin-left: rpx(-10);
+    border: none; /* 移除边框 */
+    background-image: url('@/assets/programming/xiaozhi.png'); 
+    background-size: 100%; /* 背景图片完全覆盖按钮 */
+    background-position: center; /* 背景图片居中 */
+    background-repeat: no-repeat; /* 背景图片不重复 */
+    background-color: transparent; /* 透明背景 */
+    &:hover {
+      transform: scale(1.1); /* 悬停时放大效果 */
+    }
+    &.hover,
+    &:active {
+      transform: scale(1.1); /* 激活时放大效果 */
+      box-shadow: none; /* 移除阴影效果 */
+    }
+  }
+  
+  :deep(.el-slider__stop) {
+    width: rpx(10); /* 停止点大小 */
+    height: rpx(10); /* 停止点大小 */
+    background-color: white; /* 停止点颜色 */
+    opacity: 0.2;
+  }
+}
+
+.btn-image {
+  width: rpx(40);
+  height: rpx(40);
+  object-fit: contain;
+}
+</style>

+ 1103 - 0
src/views/laboratory/ExperimentalInterface.vue

@@ -0,0 +1,1103 @@
+<!-- 实验界面 -->
+ <template>
+  <!-- 编程课程视频页面 -->
+  <div class="home-container">
+    <div class="content-box">
+      <div class="box-1">
+        <div class="inner-box left-box">
+          <div class="box-icon" @click="emit('closeVideo')">
+            <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+            返回
+          </div>
+        </div>
+      </div>
+
+      <div class="box-2">
+        <!-- 课程标题 -->
+        <div class="small-title">
+          <span>{{ course.courseName }}</span>
+        </div>
+
+        <el-empty v-if="course.isDisabled"
+                  image-size="500"
+          description="您无权查看该课程!"
+          :image="isDisabledImage"
+        />
+
+        <template v-else>
+          <!-- 视频组件 -->
+          <VideoPlayer
+              v-if="course.courseContentType === 'video'"
+              :contentType="course.courseContentType"
+              :videoPath="course.courseContent"
+              :courseId="course.id || ''"
+              :typeId="course.typeId"
+              :courseConfigList="course.courseConfigList || []"
+              :allIndices="flattenMenuItems()"
+              :currentIndex="course.key || ''"
+              @timeUpdate="handleVideoTimeUpdate"
+              @videoEnded="handleVideoEnded"
+              @saveProgress="handleSaveProgress"
+              v-memo="[course.id, course.courseContentType, course.courseContent]"
+          />
+          <!-- 图片 -->
+          <ImageView v-if="course.courseContentType === 'image'" :imagePath="course.courseContent" altText="课程图片"></ImageView>
+
+          <!-- PPT -->
+          <PptView v-if="course.courseContentType === 'ppt'" :pptPath="course.courseContent" ref="pptRef"></PptView>
+
+          <!--文生文-->
+          <TextToText class="contentClass" v-if="course.courseContentType === 'aiTextToText'" ref="aiTextToText"></TextToText>
+
+          <!--文生图-->
+          <TextToImage class="contentClass" v-if="course.courseContentType === 'aiTextToImage'" ref="aiTextToImage"></TextToImage>
+
+          <!--图生图-->
+          <ImageToImage class="contentClass" v-if="course.courseContentType === 'aiImageToImage'" ref="aiImageToImage"></ImageToImage>
+
+          <!--图生视频-->
+          <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"
+                   :route-list="course.blocklyRouteList"
+                   :user-image="course.blocklyUserImage"
+                   :info="course.blocklyInfo"
+                   :game-title="course.courseName"
+                   :course-list="props.courseList"
+                   :blockly-special-blocks="course.blocklySpecialBlocks"
+                   :current-index="currentCourseIndex"
+                   @close-game="emit('closeVideo')"
+                   @prev-section="playPreviousVideo"
+                   @next-section="playNextVideo"
+                   @saveProgress="handleSaveProgress"
+                   v-memo="[course.id, course.courseContentType]"
+          ></MapGame>
+
+        </template>
+
+        <!-- 视频切换按钮 - 始终显示 -->
+        <div class="video-switch" :class="{'map-game-mode': course.courseContentType === 'blockly'}">
+          <div class="caret-left" @click="playPreviousVideo">
+            <el-button type="warning" round :disabled="currentCourseIndex === 0">
+              <img :src="leftImg" alt="Left" />上一节</el-button 
+            >
+          </div>
+          <div class="caret-right" @click="playNextVideo">
+            <el-button type="warning" round :disabled="currentCourseIndex === props.courseList.length - 1"
+              >下一节<img :src="rightImg" alt="Right" />
+            </el-button>
+          </div>
+        </div>
+
+      </div>
+    </div>
+
+    <!-- 弹框组件 -->
+    <DialogComponents
+      componentType="blockly"
+      :questionDialogVisible="questionDialogVisible"
+      :currentQuestion="courseConfig"
+      :gradeId="gradeId"
+      :typeId="typeId"
+      :courseId="course.id || ''" 
+      @closeQuestionDialog="closeQuestionDialog"
+      @submitAnswer="handleSubmitAnswer"
+    />
+
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeftBold } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import isDisabledImage from '@/assets/images/permission/isDisabled.png'
+import { ClassType } from '@/api/class.js'
+import { Message } from '@/utils/message/Message.js'
+import { saveRecordBlockly } from '@/api/personalized/index.js'
+// 导入图标
+import leftImg from '@/assets/icon/backward.png'
+import rightImg from '@/assets/icon/f-backward.png'
+// 导入新创建的组件
+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'
+// AI实验室
+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 "@/components/blockly/MapGame.vue";
+// 根据ID获取课程列表
+import { getBlocklyByTypeId } from '@/api/programming/index.js'
+
+// 定义常量
+const CONSTANTS = {
+  ANIMATION_DURATION: '0.3s',
+  STORAGE_KEYS: {
+    WATCHED_COURSES: 'watchedCourseIds' 
+  }
+}
+
+const router = useRouter() // 获取当前路由对象
+// 渲染页面标题
+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([])
+// 已观看课程ID列表
+const watchedCourseIds = ref([])
+// 试题弹框显示状态
+const questionDialogVisible = ref(false)
+// 当前显示的试题
+const courseConfig = ref({})
+// 年级id
+const gradeId = ref('')
+// 课程大纲id
+const typeId = ref('')
+// 课程小节id
+const courseId = ref('')
+// 课程排序
+const typeSort = ref('')
+
+// 计算当前课程在列表中的索引
+const currentCourseIndex = computed(() => {
+  if (!props.courseList || !course.value.id) return -1
+  return props.courseList.findIndex(item => item.id === course.value.id)
+})
+
+// 展平所有菜单项索引
+const flattenMenuItems = () => {
+  const indices = []
+  const traverse = items => {
+    for (const item of items) {
+      if (!item.children) {
+        indices.push(item.key)
+      } else {
+        traverse(item.children)
+      }
+    }
+  }
+  traverse(menuItems.value)
+  return indices
+}
+
+// 播放上一个视频
+const playPreviousVideo = () => {
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = currentCourseIndex.value
+    if (currentIndex > 0) {
+      const previousCourse = props.courseList[currentIndex - 1]
+      // 更新当前课程数据
+      handleParentCourseData(previousCourse)
+      courseId.value = course.value.id
+      // 更新标题
+      boxIconTitle.value = course.value.courseName
+    }
+  }
+}
+
+// 播放下一个视频
+const playNextVideo = () => {
+  if (props.courseList && props.courseList.length > 0) {
+    const currentIndex = currentCourseIndex.value
+    if (currentIndex !== -1 && currentIndex < props.courseList.length - 1) {
+      const nextCourse = props.courseList[currentIndex + 1]
+
+      // 更新当前课程数据
+      handleParentCourseData(nextCourse)
+      courseId.value = course.value.id
+      // 更新标题
+      boxIconTitle.value = course.value.courseName
+    }
+  }
+}
+
+// 播放结束
+const handleVideoEnded = () => {
+  // 记录当前视频ID为已观看
+  if (
+    course.value &&
+    course.value.id &&
+    !watchedCourseIds.value.includes(course.value.id)
+  ) {
+    watchedCourseIds.value.push(course.value.id)
+    localStorage.setItem(
+      CONSTANTS.STORAGE_KEYS.WATCHED_COURSES,
+      JSON.stringify(watchedCourseIds.value)
+    )
+  }
+
+  // 自动播放下一个
+  // playNextVideo();
+}
+
+// 处理视频时间更新事件
+const handleVideoTimeUpdate = async ({ currentTime, progressPercentage, courseConfig: config }) => {
+  if (config) {
+    questionDialogVisible.value = true
+    courseConfig.value = config
+    // 保存试题进度
+    try {
+      // 确保courseId已经设置
+      if (!courseId.value && typeId.value) {
+        const courseRes = await ClassType(typeId.value)
+        if (courseRes.data && courseRes.data.length > 0) {
+          courseId.value = course.value && course.value.id ? course.value.id : courseRes.data[0].id
+        }
+      }
+      if (config.id) {
+        // 保存弹窗问题进度
+        await saveRecordBlockly({
+          brpZtId: props.courseData?.ztId,
+          brpCtId: props.courseData?.acType,
+          brpCourseConfigId: config.id,
+          brpCourseId: courseId.value,
+          brpType: 'courseQuest',
+          brpProgress: 1
+        })
+      }
+    } catch (error) {
+      console.error('保存试题进度失败:', error)
+    }
+  }
+}
+
+// 关闭试题弹框
+const closeQuestionDialog = () => {
+  questionDialogVisible.value = false
+}
+
+// 提交答案
+const handleSubmitAnswer = ({ selectedOption }) => {
+  questionDialogVisible.value = false
+}
+
+// 处理父组件传递的课程数据
+const handleParentCourseData = (courseData = props.courseData) => {
+  if (!courseData) return false
+  // 设置返回按钮标题
+  boxIconTitle.value = courseData.acName
+  // 重新定义course接收传递过来的数据
+  course.value = {
+    id: courseData.id,
+    courseName: courseData.acName,
+    typeId: courseData.acType,
+    courseContentType: courseData.acContentType,
+    courseContent: courseData.acContent,
+    courseConfigList: courseData.aiCourseConfigList,
+    key: courseData.id.toString(),
+    // blockly相关属性,用于MapGame组件
+    blocklyBackground: courseData.aiCourseBackground,
+    blocklyTileSize: courseData.aiCourseTileSize,
+    blocklyStartPoint: courseData.aiCourseStartPoint,
+    blocklyEndPoint: courseData.aiCourseEndPoint,
+    blocklyWalkablePoints: courseData.aiCourseWalkablePoints,
+    blocklyUserDirection: courseData.aiCourseUserDirection || 0,
+    blocklyRouteList: courseData.aiCourseRouteList,
+    blocklyUserImage: courseData.aiCourseUserImage,
+    blocklyInfo: courseData.aiCourseInfo,
+    blocklySpecialBlocks: courseData.aiCourseSpecialBlocks ? courseData.aiCourseSpecialBlocks.split(',') : [],
+    isDisabled: courseData.isDisabled,
+  }
+  courseId.value = course.value.id
+  // 如果有配置,禁用视频检查
+  if (courseData.isDisabled) {
+    Message().notifyWarning('您的账号并未开放此课程!', true)
+  }
+  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
+  })
+}
+
+// 初始化已观看课程ID
+const initWatchedCourseIds = () => {
+  const savedWatchedIds = localStorage.getItem(CONSTANTS.STORAGE_KEYS.WATCHED_COURSES)
+  if (savedWatchedIds) {
+    try {
+      watchedCourseIds.value = JSON.parse(savedWatchedIds)
+    } catch (error) {
+      console.error('解析已观看课程ID失败:', error)
+      watchedCourseIds.value = []
+    }
+  }
+}
+
+// 渲染 课程数据结构 以及 视频
+onMounted(async () => {
+  // 初始化已观看课程ID
+  initWatchedCourseIds()
+
+  // 检查是否有从父组件传递的courseData
+  if (handleParentCourseData()) {
+    return
+  }
+
+  // 从路由参数获取typeId
+  const typeIdParam = router.currentRoute.value.query.typeId
+
+  if (typeIdParam) {
+    typeId.value = typeIdParam
+    try {
+      // 获取课程列表
+      const res = await getBlocklyByTypeId(typeIdParam)
+      if (res && res.data && Array.isArray(res.data)) {
+        // 保存原始API返回的数据
+        handleParentCourseData(res.data[0])
+
+        // 处理课程数据
+        courseList.value = processCourseDataList(res.data)
+      }
+    } catch (error) {
+      console.error('获取课程数据失败:', error)
+      ElMessage.error('获取课程数据失败,请稍后重试')
+    }
+  }
+
+  // 设置页面标题和排序
+  const title = router.currentRoute.value.query.typeName
+  if (title) {
+    boxIconTitle.value = String(title)
+  }
+  typeSort.value = router.currentRoute.value.query.typeSort
+})
+
+// 清理函数
+onUnmounted(() => {})
+
+// 保存视频/bockly进度接口
+const handleSaveProgress = async (type, progress) => {
+  try {
+    await saveRecordBlockly({
+      brpZtId: props.courseData?.ztId,
+      brpCtId: props.courseData?.acType,
+      brpCourseId: course.value.id,
+      brpType: type,
+      brpProgress: progress
+    })
+  } catch (error) {
+    console.error(`保存${type}进度失败:`, error)
+  }
+}
+
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+@use 'sass:color'; // 引入 color 模块
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+// 定义儿童风格的蓝紫色调
+$primary-color: rgba(106, 90, 205, 0.52); // 主色调:蓝紫色
+$secondary-color: rgba(147, 112, 219, 0.66); // 辅助色:亮蓝紫色
+$accent-color: rgb(133, 89, 220); // 强调色:暗蓝紫色
+$light-color: #ffffff; // 浅色背景:淡紫色
+$text-color: #483d8b; // 文本颜色:靛蓝色
+
+
+// 视频切换按钮样式
+.video-switch {
+  width: 100%;
+  display: flex;
+  margin-top: rpx(5);
+  margin-bottom: rpx(15);
+  justify-content: center;
+}
+
+// MapGame地图模式下-上下节切换按钮-显示到右上角
+.video-switch.map-game-mode {
+  position: absolute;
+  top: rpx(5);
+  right: rpx(2);
+  width: auto;
+  justify-content: flex-end;
+  z-index: 1000;
+  margin: rpx(10);
+  gap: rpx(10);
+}
+
+
+.caret-right,
+.caret-left {
+  width: rpx(50);
+   margin: 0 rpx(20);
+  margin: auto;
+  display: flex;
+  justify-content: center;
+}
+
+.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);
+}
+
+.default-messages {
+  margin-top: rpx(-10);
+  margin-bottom: rpx(5);
+}
+
+
+.content-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column; /* 子元素上下排列 */
+  background-image: url('@/assets/programming/list_bg03.png');
+  overflow-y: auto;
+  // 滚动条整体样式
+  &::-webkit-scrollbar {
+    width: rpx(1); // 滚动条宽度
+  }
+  // 滚动条滑块样式
+  &::-webkit-scrollbar-thumb {
+    background-color: rgba(255, 255, 255, 0.5); // 滑块颜色
+    border-radius: 4px; // 滑块圆角
+    transition: background-color 0.3s ease;
+  }
+  // 滚动条轨道样式
+  &::-webkit-scrollbar-track {
+    background-color: rgba(0, 0, 0, 0.1); // 轨道颜色
+    border-radius: 4px; // 轨道圆角
+  }
+}
+.home-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-image: url('@/assets/programming/list_bg03.png');
+}
+
+.box-1 {
+  width: 100%;
+  // height: rpx(50);
+  margin-top: rpx(10);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  box-sizing: border-box;
+  font-size: rpx(15); // 默认字体大小
+}
+
+.inner-box {
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: rpx(16); // 默认字体大小
+}
+
+.left-box {
+  position: relative;
+  justify-content: flex-start;
+  align-items: flex-start;
+  flex: 1; // 设置左侧盒子占比为 2
+  cursor: pointer; // 添加鼠标指针样式
+}
+.box-icon {
+  display: flex;
+  align-items: center;
+  margin-left: rpx(7);
+  gap: 10px;
+  padding: 10px 20px;
+  background-color: rgba(255, 255, 255, 0.8);
+  border-radius: 30px;
+  backdrop-filter: blur(10px);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-size: 16px;
+  color: #333;
+  font-weight: 500;
+  width: fit-content;
+}
+
+.box-icon:hover {
+  background-color: rgba(255, 255, 255, 0.9);
+  transform: translate(-3px);
+}
+
+.box-icon .left-icon {
+  margin: 0;
+}
+
+.left-box span {
+  position: absolute;
+  font-size: rpx(11); // 默认字体大小
+  color: white;
+}
+
+
+.box-2 {
+  width: 100%;
+  flex: 1;
+  box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
+  box-sizing: border-box;
+  // display: flex; // 确保子元素水平排列
+  flex-wrap: wrap; // 允许子元素换行;
+  align-content: center; // 顶部对齐;
+  cursor: pointer; // 添加鼠标指针样式
+}
+
+.small-title {
+  width: 100%;
+  // margin-top: rpx(-20);
+  height: rpx(15);
+  color: white;
+  font-size: rpx(10);
+  justify-content: center; //使子元素水平居中
+}
+// 图片容器样式
+.image-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  // padding: rpx(20) 0;
+}
+
+// 图片样式
+.course-image {
+  max-width: 70%;
+  max-height: rpx(400);
+  object-fit: contain;
+  border-radius: rpx(12);
+  box-shadow: 0 rpx(10) rpx(20) rgba(0, 0, 0, 0.1);
+}
+
+
+// 儿童风格试题弹框样式
+.child-dialog {
+  .el-dialog__header {
+    display: none; // 隐藏原有的标题栏
+  }
+
+  .el-dialog__body {
+    padding: rpx(20);
+    position: relative;
+  }
+
+  .el-dialog__footer {
+    border-top: none;
+    padding: rpx(10) rpx(20);
+    text-align: center;
+    margin-top: auto; // 使底部按钮位于底部
+  }
+
+  .el-dialog__wrapper {
+    // 修改半透明背景色
+    background-color: rgba(0, 0, 0, 0.6);
+  }
+
+  .el-dialog {
+    border: none;
+    border-radius: rpx(20);
+    background: linear-gradient(
+      135deg,
+      $light-color,
+      #d8bfd8
+    ); // 柔和的蓝紫色渐变
+    overflow: hidden;
+    display: flex; // 添加 flex 布局
+    flex-direction: column; // 设置垂直布局
+    min-height: 0; // 防止子元素溢出
+    // 添加装饰元素
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: rpx(10);
+      background: linear-gradient(90deg, $secondary-color, $accent-color);
+    }
+  }
+}
+
+// 问题标题样式
+.question-title {
+  padding: rpx(15);
+  border-radius: rpx(12);
+  margin-bottom: rpx(20);
+  color: #483d8b;
+  font-weight: bold;
+  font-size: rpx(12);
+  position: relative;
+  display: flex;
+
+  .question-icon {
+    background-color: $accent-color;
+    color: white;
+    width: rpx(24);
+    height: rpx(24);
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: rpx(10);
+    font-weight: bold;
+    box-shadow: 0 rpx(2) rpx(5) rgba($accent-color, 0.3);
+  }
+}
+
+// 选项容器样式
+.options-container {
+  margin-bottom: rpx(20);
+}
+
+// 问题选项样式
+.question-option {
+  margin: rpx(8) 0;
+  padding: rpx(10) rpx(15);
+  border-radius: rpx(12);
+  border: rpx(1) solid rgba($primary-color, 0.3);
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  background-color: white;
+  box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.05);
+
+  ::v-deep(.el-radio__label) {
+    color: $text-color;
+    margin-left: rpx(8);
+    flex: 1;
+    text-align: left;
+    // 增大字体大小
+    font-size: rpx(12);
+  }
+
+  // 选中时的样式变化
+  .el-radio__input.is-checked + .el-radio__label {
+    font-weight: bold;
+    color: $accent-color;
+  }
+
+  &:hover {
+    background-color: rgba($primary-color, 0.05);
+    border-color: rgba($primary-color, 0.5);
+    transform: translateY(-rpx(1));
+  }
+}
+
+// 暂无选项样式
+.no-options {
+  color: rgba($text-color, 0.7);
+  text-align: center;
+  padding: rpx(20);
+  font-size: rpx(12);
+}
+
+// 底部按钮样式
+.child-button {
+  min-width: rpx(80);
+  height: rpx(30);
+  border-radius: rpx(8);
+  font-size: rpx(12);
+  font-weight: 500;
+  transition: all 0.3s ease;
+  box-shadow: 0 rpx(2) rpx(8) rgba(0, 0, 0, 0.1);
+
+  &.confirm {
+    background: linear-gradient(to bottom, #ab81ff, #8559dc);
+    border: none;
+    border-right: 15px;
+    color: white;
+
+    &:hover {
+      background: linear-gradient(
+        to bottom,
+        color.adjust(#ab81ff, $lightness: -5%),
+        color.adjust(#8559dc, $lightness: -5%)
+      );
+      transform: translateY(-rpx(1));
+      color: white;
+    }
+  }
+
+  &.cancel {
+    background: white;
+    border: rpx(1) solid rgba($primary-color, 0.3);
+    color: $text-color;
+
+    &:hover {
+      background: rgba($primary-color, 0.05);
+      border-color: rgba($primary-color, 0.5);
+      transform: translateY(-rpx(1));
+    }
+  }
+}
+
+// AI对话图标样式
+.ai-icon-container {
+  position: absolute;
+  bottom: rpx(20);
+  right: rpx(20);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  cursor: pointer;
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-rpx(2));
+  }
+
+  .ai-icon {
+    width: rpx(30);
+    height: rpx(30);
+    margin-bottom: rpx(0);
+    filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
+    // 添加过渡动画
+    transition: transform 0.3s ease;
+  }
+
+  // 悬浮时放大效果
+  .ai-icon:hover {
+    transform: scale(1.5);
+  }
+
+  .ai-text {
+    color: $text-color;
+    font-size: rpx(8);
+    background-color: rgba(255, 255, 255, 0.7);
+    padding: rpx(2) rpx(5);
+    border-radius: rpx(5);
+  }
+}
+
+// AI消息样式
+.ai-message {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: rpx(15);
+
+  .ai-avatar {
+    width: rpx(30);
+    height: rpx(30);
+    border-radius: 50%;
+    margin-right: rpx(10);
+    background-color: $primary-color;
+    padding: rpx(5);
+  }
+
+  .ai-text-content {
+    background-color: $light-color;
+    padding: rpx(8) rpx(12);
+    border-radius: rpx(10);
+    font-size: rpx(10);
+    color: $text-color;
+    max-width: 80%;
+  }
+}
+
+// 用户输入框样式
+.user-input {
+  ::v-deep(.el-input__wrapper) {
+    height: rpx(23);
+    border-top-left-radius: rpx(5);
+    border-bottom-left-radius: rpx(5);
+    border-color: rgba($primary-color, 0.3);
+
+    &:focus-within {
+      box-shadow: 0 0 0 rpx(1) rgba($primary-color, 0.5);
+    }
+  }
+
+  ::v-deep(.el-input__inner) {
+    font-size: rpx(10);
+    // color: $text-color;
+    text-indent: 1em;
+  }
+  ::v-deep(.el-input-group__append, .el-input-group__prepend) {
+    background: linear-gradient(to bottom, #ab81ff, #8559dc);
+    border-top-right-radius: rpx(5);
+    border-bottom-right-radius: rpx(5);
+    color: white;
+    font-size: rpx(9);
+  }
+}
+/* 定义淡入和缩放动画 */
+.fade-scale-enter-active,
+.fade-scale-leave-active {
+  transition: all 0.5s ease;
+}
+
+.fade-scale-enter-from,
+.fade-scale-leave-to {
+  opacity: 0.1;
+  transform: scale(0.9);
+}
+
+// 自定义试题弹框背景
+.child-dialog-wrapper {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.6); // 半透明背景
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+}
+
+.child-dialog {
+  border: none;
+  border-radius: rpx(15);
+  background: rgb(255, 255, 255, 0.8); // 柔和的蓝紫色渐变
+  overflow: hidden;
+  padding: rpx(5);
+  // width: 40%;
+  // height: 60%;
+  position: relative;
+}
+
+// AI对话弹框样式
+.ai-dialog-wrapper {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  z-index: 1001;
+  pointer-events: none;
+}
+
+.ai-dialog {
+  border: none;
+  border-radius: rpx(15);
+  background: rgb(255, 255, 255, 0.8);
+  overflow: hidden;
+  padding: rpx(20);
+  width: 30%;
+  // 增加高度
+  height: 80%;
+  margin-right: rpx(50);
+  pointer-events: auto;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: rpx(10);
+  }
+}
+
+.ai-dialog-header {
+  position: relative;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-bottom: rpx(15);
+  margin-top: rpx(-10);
+
+  img {
+    width: rpx(15);
+  }
+
+  h3 {
+    color: black;
+    font-size: rpx(12);
+  }
+
+  .close-btn {
+    padding: 0;
+    width: rpx(20);
+    height: rpx(20);
+    font-size: rpx(16);
+    line-height: 1;
+    position: absolute;
+    background-color: transparent;
+    border: none;
+    margin-left: rpx(200);
+  }
+}
+
+.ai-dialog-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.ai-message-history {
+  flex: 1;
+  // 当内容超出容器高度时,显示垂直滚动条
+  overflow-y: auto;
+  margin-bottom: rpx(15);
+  // 可以根据实际情况调整最大高度
+  font-size: rpx(5);
+  max-height: 50vh;
+
+  .message {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: rpx(10);
+
+    &.user {
+      flex-direction: row-reverse;
+    }
+
+    .avatar {
+      width: rpx(30);
+      height: rpx(30);
+      border-radius: 50%;
+      margin: 0 rpx(10);
+    }
+
+    .user {
+      width: 15px;
+      height: 15px;
+    }
+
+    .message-content {
+      background-color: #ffffff;
+      font-size: rpx(5);
+      max-width: 80%;
+      color: black;
+      box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
+    }
+  }
+  // 滚动条整体样式
+  &::-webkit-scrollbar {
+    width: rpx(4); // 滚动条宽度
+  }
+
+  // 滚动条滑块样式
+  &::-webkit-scrollbar-thumb {
+    background-color: $primary-color; // 滑块颜色
+    border-radius: rpx(4); // 滑块圆角
+    transition: background-color 0.3s ease; // 颜色过渡效果
+  }
+
+  // 滚动条轨道样式
+  &::-webkit-scrollbar-track {
+    background-color: rgba($primary-color, 0.2); // 轨道颜色
+    border-radius: rpx(4); // 轨道圆角
+  }
+
+  .message {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: rpx(10);
+
+    &.user {
+      flex-direction: row-reverse;
+    }
+
+    .avatar {
+      width: rpx(30);
+      height: rpx(30);
+      border-radius: 50%;
+      margin: 0 rpx(10);
+    }
+
+    .message-content {
+      background-color: white;
+      padding: rpx(8) rpx(12);
+      border-radius: rpx(5);
+      font-size: rpx(8);
+      color: black;
+      max-width: 80%;
+      box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
+    }
+  }
+}
+
+// 优化发送按钮样式
+.send-button {
+  border: none;
+  color: white;
+}
+</style>
+
+
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.default-messages {
+  margin-top: rpx(-10);
+  margin-bottom: rpx(5);
+}
+
+
+.contentClass{
+  width: 70%;
+  height: 80%;
+  margin: 0 auto;
+  border-radius: rpx(15);
+  overflow: hidden;
+}
+</style>

+ 637 - 0
src/views/laboratory/ExperimentalTheme.vue

@@ -0,0 +1,637 @@
+<!-- 实验室主题 -->
+<template>
+    <div class="programming-content">
+      <!-- 标题部分 -->
+      <div class="top-box">
+        <div class="top-left-box">
+          <div class="top-left-inner-box" >
+            <!-- 左侧返回图标 -->
+            <div class="left-content-wrapper" @click="goToHomePage">
+              <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
+              <span class="left-text">首页</span>
+            </div>
+          </div>
+        </div>
+        <div class="top-center-box">
+          <div class="top-center-inner-box">
+            <span>AI实验课</span>
+          </div>
+        </div>
+        <div class="top-right-box">
+          <div class="top-right-inner-box">
+            <!-- 用户名显示 -->
+            <div class="user-name-box" v-if="userName">
+              {{ userName }}
+            </div>
+            <!-- 退出登录 -->
+            <el-button round class="logout-box-btn" @click="LogoutClick()">
+              <img :src="logoutIcon" alt="Logout" />
+              退出登录
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+        <!-- 编程部分 -->
+        <div class="middle-wrapper">
+          <div 
+            class="middle-box"
+            ref="middleBox"
+            @mousedown="handleMouseDown"
+            @mousemove="handleMouseMove"
+            @mouseup="handleMouseUp"
+            @mouseleave="handleMouseLeave"
+            @scroll="handleScroll"
+          >
+            <div 
+              v-for="(experiment, index) in experimentList" 
+              :key="experiment.id" 
+              class="middle-inner-box"
+              @click.stop="activeButton = index"
+              v-memo="[activeButton === index]"
+            >
+              <div 
+                class="child-box-with-bg" 
+                :style="{ backgroundImage: `url(${experiment.bgImage})` }" 
+                :class="{ 'active': activeButton === index }" 
+                @click="goToProgrammingList(experiment, index)"
+              >
+                <div class="box-title">
+                  <span>{{ experiment.title }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 底部切换按钮 -->
+        <div class="bottom-box">
+          <!-- 左切换按钮 -->
+          <div 
+            class="carousel-btn prev-btn" 
+            @click="prevExperiment" 
+            :class="{ 'disabled-btn': activeButton === 0 }" 
+            :disabled="activeButton === 0"
+          >
+            <img :src="leftbtn" alt="左按钮" class="btn-image" :class="{ 'disabled-icon': activeButton === 0 }" />
+          </div>
+          <!-- 进度条滑块 -->
+          <div class="line-container" v-if="circleButtons.length > 0">
+            <el-slider 
+              v-model="activeButton" 
+              :max="circleButtons.length - 1" 
+              :step="1" 
+              :show-tooltip="false" 
+            />
+          </div>
+          <!-- 右切换按钮 -->
+          <div 
+            class="carousel-btn next-btn" 
+            @click="nextExperiment" 
+            :class="{ 'disabled-btn': activeButton >= experimentList.length - 1 }" 
+            :disabled="activeButton >= experimentList.length - 1"
+          >
+            <img :src="rightbtn" alt="右按钮" class="btn-image" :class="{ 'disabled-icon': activeButton >= experimentList.length - 1 }" />
+          </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onMounted, computed, onUnmounted } from 'vue'
+// 返回图标
+import { ArrowLeftBold } from '@element-plus/icons-vue';
+// 导入路由
+import { useRouter } from 'vue-router';
+// 导入按钮图片
+import leftbtn from '@/assets/programming/leftbtn.png'
+import rightbtn from '@/assets/programming/rightbtn.png'
+// 退出登录图标
+import logoutIcon from '@/assets/icon/logout.png'
+// 退出登录
+import { logout } from '@/api/login/login.js'
+import { Message } from '@/utils/message/Message.js'
+// 实验课主题接口
+import { getThemeExperimentList } from '@/api/laboratory/index.js'
+
+// 常量定义
+const CONSTANTS = {
+  SCROLL_SPEED: 2, // 滚动速度
+  SCALE_FACTOR: 1.1,
+  ANIMATION_DURATION: '0.3s'
+}
+
+// 创建路由实例
+const router = useRouter()
+// 当前激活的按钮索引
+const activeButton = ref(0)
+// 定义实验主题数据
+const experimentList = reactive([])
+// 用户名响应式变量
+const userName = ref('')
+// 获取middleBox元素的引用
+const middleBox = ref(null)
+// 拖动相关变量
+const isDragging = ref(false)
+const startX = ref(0)
+const scrollLeft = ref(0)
+
+// 计算属性:根据实验列表生成圆形按钮数据
+const circleButtons = computed(() => {
+  return experimentList.map((_, index) => ({ 
+    text: String(index + 1) 
+  }))
+})
+
+// 更新用户名
+const updateUserName = () => {
+  userName.value = localStorage.getItem('userName') || ''
+}
+
+// 获取实验课主题列表
+const fetchExperimentList = async () => {
+  try {
+    const res = await getThemeExperimentList()
+    if (res && res.data) {
+      // 清空现有数据
+      experimentList.length = 0;
+      res.data.forEach(item => {
+        experimentList.push({
+          id: item.id,
+          title: item.ctType,
+          bgImage: item.ctTypeImage
+        });
+      });
+    }
+  } catch (error) {
+    console.error('获取实验主题列表失败:', error)
+    Message().notifyError('获取实验主题列表失败,请刷新页面重试', true)
+  }
+}
+
+// 监听activeButton变化,切换数据项时的居中显示和放大效果
+watch(activeButton, (newIndex) => {
+  if (middleBox.value) {
+    // 找到对应的课程卡片元素
+    const courseElement = middleBox.value.querySelector(`.middle-inner-box:nth-child(${newIndex + 1})`);
+    if (courseElement) {
+      // 计算滚动位置,选中的卡片居中
+      const containerWidth = middleBox.value.clientWidth;
+      const elementLeft = courseElement.offsetLeft;
+      const elementWidth = courseElement.offsetWidth;
+      // 计算居中位置
+      const scrollPosition = elementLeft - (containerWidth / 2) + (elementWidth / 2);
+      // 平滑滚动到计算的位置
+      middleBox.value.scrollTo({
+        left: scrollPosition,
+        behavior: 'smooth'
+      });
+    }
+  }
+});
+
+// 鼠标按下事件处理函数
+const handleMouseDown = (e) => {
+  isDragging.value = true
+  startX.value = e.pageX - middleBox.value.offsetLeft
+  scrollLeft.value = middleBox.value.scrollLeft
+  // 抓取光标样式
+  middleBox.value.style.cursor = 'grabbing'
+}
+
+// 鼠标移动事件处理函数
+const handleMouseMove = (e) => {
+  if (!isDragging.value) return
+  e.preventDefault()
+  const x = e.pageX - middleBox.value.offsetLeft
+  const walk = (x - startX.value) * CONSTANTS.SCROLL_SPEED // 滚动速度
+  middleBox.value.scrollLeft = scrollLeft.value - walk
+}
+
+// 鼠标松开事件处理函数
+const handleMouseUp = () => {
+  isDragging.value = false
+  // 恢复默认光标样式
+  middleBox.value.style.cursor = 'grab'
+}
+
+// 鼠标离开事件处理函数
+const handleMouseLeave = () => {
+  isDragging.value = false
+  // 恢复默认光标样式
+  if (middleBox.value) {
+    middleBox.value.style.cursor = 'grab'
+  }
+}
+
+// 滚动事件处理函数
+const handleScroll = () => {}
+
+// 左按钮
+const prevExperiment = () => {
+  if (activeButton.value > 0) {
+    activeButton.value -= 1;
+  }
+}
+
+// 右按钮
+const nextExperiment = () => {
+  if (activeButton.value < experimentList.length - 1) {
+    activeButton.value += 1;
+  }
+}
+
+// 跳转到实验类型页面
+const goToProgrammingList = (experiment, index) => {
+  router.push({
+    path: '/experiment-type',
+    state: {
+      themeIndex: index,
+      themeTitle: experiment.title,
+      themeId: experiment.id
+    }
+  })
+}
+
+// 返回到首页
+const goToHomePage = () => {
+  router.push({ path: '/home' })
+}
+
+// 退出登录
+const LogoutClick = async () => {
+  try {
+    // 调用logout API 退出登录
+    const logoutRes = await logout()
+    console.log('退出登录:', logoutRes);
+    // 清空登录状态相关信息
+    localStorage.removeItem('token')
+    localStorage.removeItem('isLoggedIn')
+    localStorage.removeItem('maxCourseSections')
+    
+    // 检查是否勾选了记住我
+    const rememberMe = localStorage.getItem('rememberMe') === 'true'
+    
+    if (!rememberMe) {
+      // 未勾选记住我,清空所有信息
+      localStorage.removeItem('userName')
+      localStorage.removeItem('tenantName')
+      localStorage.removeItem('rememberMe')
+    }
+
+    // 跳转到登录页面
+    router.push({ path: '/login' })
+  } catch (error) {
+    console.error('退出登录失败:', error)
+    // API调用失败,也清空本地存储
+    localStorage.removeItem('token')
+    localStorage.removeItem('isLoggedIn')
+    localStorage.removeItem('maxCourseSections')
+    
+    // 检查是否勾选了记住我
+    const rememberMe = localStorage.getItem('rememberMe') === 'true'
+    
+    if (!rememberMe) {
+      // 未勾选记住我,清空所有信息
+      localStorage.removeItem('userName')
+      localStorage.removeItem('tenantName')
+      localStorage.removeItem('rememberMe')
+    }
+    Message().notifyError('退出登录失败,请重试', true)
+    router.push({ path: '/login' })
+  }
+}
+
+// 存储事件处理函数
+const handleStorageChange = (e) => {
+  if (e.key === 'userName') {
+    updateUserName()
+  }
+}
+
+onMounted(() => {
+  // 初始化用户名
+  updateUserName()
+  // 获取实验主题列表
+  fetchExperimentList()
+  // storage事件监听器,监听其他标签页对localStorage的修改
+  window.addEventListener('storage', handleStorageChange)
+})
+
+onUnmounted(() => {
+  // 移除事件监听器
+  window.removeEventListener('storage', handleStorageChange)
+})
+
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+.programming-content{
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-image: url('@/assets/laboratory/laboratory-bg.png');
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  display: flex;
+  flex-direction: column;
+}
+.top-box {
+  height: 20%;
+  display: flex;
+}
+.top-left-box,
+.top-right-box {
+  flex: 1;
+  height: 50%;
+  border-radius: rpx(5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.top-center-box {
+  flex: 2;
+  border-radius: rpx(5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.top-left-inner-box,
+.top-center-inner-box,
+.top-right-inner-box {
+  width: 100%;
+  height: 100%;
+}
+.top-center-inner-box{
+    background-image: url('@/assets/programming/list_title.png');
+    background-size: 70%; 
+    background-repeat: no-repeat;
+    background-position: center center; /* 背景图垂直水平居中 */
+    display: flex;
+    align-items: center; /* 垂直居中 */
+    justify-content: center; /* 水平居中 */
+    cursor: pointer;
+    position: relative; /* 相对定位 */
+    span{
+        font-size: rpx(16);
+        color: white;
+        position: relative;
+        z-index: 1; /* 确保文字在背景图上方 */
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding-bottom: rpx(5);
+    }
+  }
+.top-left-inner-box{
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  .left-content-wrapper{
+    display: flex;
+    align-items: center; /* 保持内部元素垂直居中 */
+  }
+  .left-icon{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(20);
+    cursor: pointer;
+  }
+  .left-text{
+    font-size: rpx(14);
+    color: white;
+    padding-left: rpx(10);
+    cursor: pointer;
+  }
+}
+
+.top-right-inner-box{
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: flex-end; /* 水平靠右对齐 */
+  padding-right: rpx(0);
+}
+
+.user-name-box {
+  width: auto;
+  height: rpx(15);
+  margin: rpx(10) 0 0 0;
+  color: white;
+  font-size: rpx(8);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(255, 255, 255, 0.2);
+  border-radius: rpx(15);
+  padding: rpx(0) rpx(10);
+  min-width: rpx(20);
+  cursor: pointer;
+}
+
+.logout-box-btn {
+  width: rpx(65);
+  height: rpx(15);
+  margin: rpx(10) rpx(10) 0 0;
+  background-color: transparent;
+  color: white;
+  border: none;
+  font-size: rpx(7);
+  outline: none;
+}
+
+.logout-box-btn img {
+  width: rpx(10);
+}
+  
+.middle-wrapper {
+  height: 75%;
+  overflow: hidden; /* 溢出隐藏 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.middle-box {
+  min-width: 100%; /* 固定最小宽度 */
+  height: 100%;
+  margin: 0 rpx(50);
+  overflow-x: auto; /* 水平滚动条 */
+  overflow-y: hidden; /* 取消上下滚动 */
+  white-space: nowrap; /* 防止元素换行 */
+  scroll-behavior: smooth; /* 平滑滚动效果 */
+  -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
+  position: relative;
+  flex: 1;
+}
+
+.middle-inner-box{
+  width: rpx(200); /* 设置固定宽度 */
+  height: 100%; /* 设置固定高度 */
+  position: relative;
+  cursor: pointer;
+  user-select: none; /* 禁止文本选择 */
+  display: inline-flex; /* 使用flex布局 */
+  align-items: center; /* 垂直居中 */
+  justify-content: center; /* 水平居中 */
+}
+/* 鼠标按下时的光标样式 */
+.middle-inner-box:active {
+  cursor: grabbing;
+}
+/* 隐藏滚动条 */
+.middle-box::-webkit-scrollbar {
+  height: rpx(0);
+}
+.child-box-with-bg {
+  width: rpx(200); 
+  height: rpx(210);
+  background-size: 100%; 
+  background-position: center; 
+  background-repeat: no-repeat;
+  transition: all 0.3s ease; /* 过渡效果 */
+  margin: auto;
+  position: relative; /* 相对定位 */
+}
+
+/* 鼠标悬浮时的放大效果 */
+.child-box-with-bg:hover {
+  transform: scale(1.1); /* 放大1.1倍 */
+  z-index: 10; /* 确保放大时在顶层显示 */
+}
+
+/* 选中时的放大效果 - 加强视觉反馈 */
+.child-box-with-bg.active {
+  transform: scale(1.1); /* 选中时放大1.2倍,比悬浮效果更明显 */
+  z-index: 20; /* 确保选中时在顶层显示 */
+  transition: all 0.3s ease; /* 过渡动画效果 */
+}
+
+/* 标题样式 */
+.box-title {
+  position: absolute;
+  top: 14.29%; /* 使用百分比定位,相对于容器高度 */
+  left: 0;
+  right: 0;
+  color: white;
+  text-align: center;
+  font-size: rpx(16); 
+  letter-spacing: rpx(2); /* 字体间距 */
+  transform: translateZ(1px);
+  padding: 0 rpx(10);
+  word-wrap: break-word;
+  white-space: normal;
+  line-height: 1.2;
+}
+
+
+.bottom-box {
+  height: 15%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.line-container {
+  width: 70%; /* 滑块宽度 */
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  /* el-slider滑块样式 */
+  :deep(.el-slider__runway) {
+    height: rpx(10); /* 滑块轨道高度 */
+    background-color: rgba(228, 227, 254,0.3); /* 轨道背景色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__bar) {
+    height: rpx(10); /* 滑块激活部分高度 */
+    background-color: #2598c2; /* 激活部分颜色为蓝色 */
+    border-radius: rpx(10);
+  }
+  
+  :deep(.el-slider__button) {
+    width: rpx(35); 
+    height: rpx(35); 
+    margin-top: rpx(-5);
+    margin-left: rpx(-10);
+    border: none; /* 移除边框 */
+    background-image: url('@/assets/programming/xiaozhi.png'); 
+    background-size: 100%; /* 背景图片完全覆盖按钮 */
+    background-position: center; /* 背景图片居中 */
+    background-repeat: no-repeat; /* 背景图片不重复 */
+    background-color: transparent; /* 透明背景 */
+    &:hover {
+      transform: scale(1.1); /* 悬停时放大效果 */
+    }
+    &.hover,
+    &:active {
+      transform: scale(1.1); /* 激活时放大效果 */
+      box-shadow: none; /* 移除阴影效果 */
+    }
+  }
+  
+  :deep(.el-slider__stop) {
+    width: rpx(10); /* 停止点大小 */
+    height: rpx(10); /* 停止点大小 */
+    background-color: white; /* 停止点颜色 */
+    opacity: 0.2;
+  }
+}
+
+
+
+/* 轮播图按钮样式 */
+.carousel-btn {
+  width: rpx(40);
+  height: rpx(40);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  transition: all 0.3s ease;
+}
+
+.carousel-btn:hover:not(.disabled-btn) {
+  transform: scale(1.1);
+}
+
+.carousel-btn:disabled,
+.disabled-btn {
+  cursor: not-allowed;
+  box-shadow: none;
+}
+
+// 禁用样式
+.disabled-icon {
+  opacity: 0.5;
+}
+
+.prev-btn {
+  margin-right: rpx(5);
+}
+
+.next-btn {
+  margin-left: rpx(5);
+}
+
+.btn-image {
+  width: rpx(40);
+  height: rpx(40);
+  object-fit: contain;
+}
+
+
+
+</style>

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

@@ -457,8 +457,12 @@ onMounted(() => {
   color: white;
   text-align: center;
   font-size: rpx(18); 
-  letter-spacing: rpx(6); /* 增加字体间距 */
+  letter-spacing: rpx(2); /* 字体间距 */
   transform: translateZ(1px);
+  padding: 0 rpx(10);
+  word-wrap: break-word;
+  white-space: normal;
+  line-height: 1.2;
 }