Sfoglia il codice sorgente

剥离AI诗词课,单独给出入口

liyanbo 4 settimane fa
parent
commit
f689bc700b

BIN
src/assets/aiPoetry/poetry.png


BIN
src/assets/manage/poetry.png


+ 3 - 3
src/components/aiCourse/DialogEngine.vue

@@ -99,7 +99,7 @@ const props = defineProps({
 });
 
 // 事件定义
-const emit = defineEmits(['dialogueEnded']);
+const emit = defineEmits(['dialogueEnded', 'goBack']);
 
 /** 状态定义 **/
 // 对话位置状态
@@ -520,11 +520,11 @@ const playNext = (isAutoPlay = false) => {
 };
 
 /**
- * 返回主页面
+ * 返回主页面 - 触发父组件的 goBack 方法
  */
 const goBackToMain = () => {
   stopAllAudio();
-  router.push('/ai-general-course');
+  emit('goBack');
 };
 
 /**

+ 32 - 3
src/router/index.js

@@ -35,7 +35,7 @@ const routes = [
     path: '/management-interface',
     component: () => import('../views/ManagementInterface.vue')
   },
-  // 【通识课】首页
+  //  ============================【通识课】首页
   {
     path: '/home',
     component: () => import('../components/HomePage.vue')
@@ -142,7 +142,23 @@ const routes = [
     component: () => import('../views/AIPage/AIPoetry.vue')
   },
 
-  // 【blockly编程课】首页
+  // ============================【AI诗词课】首页
+  {
+    path: '/aiPoetryHome',
+    component: () => import('../views/AiPoetry/PoetryHome.vue')
+  },
+  // AI诗词课课程列表
+  {
+    path: '/ai-poetry-course',
+    component: () => import('../views/AiPoetry/AIPoetryCourse.vue')
+  },
+  // AI诗词课开发
+  {
+    path: '/ai-poetry-develop',
+    component: () => import('../views/AiPoetry/AIPoetryDevelop.vue')
+  },
+
+  //  ============================【blockly编程课】首页
   {
     path: '/blocklyHome',
     component: () => import('../views/programming/ProgrammingGame.vue')
@@ -238,6 +254,16 @@ const aiCourseRoutes = {
   ]
 }
 
+const aiPoetryRoutes = {
+  home: '/aiPoetryHome',
+  login: '/login',
+  role: 'aiPoetry',
+  children: [
+    '/ai-poetry-course',
+    '/ai-poetry-develop'
+  ]
+}
+
 // 管理界面路由配置
 const managementRoutes = {
   home: '/management-interface',
@@ -310,6 +336,7 @@ router.beforeEach(async (to, from, next) => {
   const hasHomePermission = roleRouteSet.includes(homeRoutes.role)
   const hasBlocklyPermission = roleRouteSet.includes(blocklyRoutes.role)
   const hasAiCoursePermission = roleRouteSet.includes(aiCourseRoutes.role)
+  const hasAiPoetryPermission = roleRouteSet.includes(aiPoetryRoutes.role)
   const hasManagementPermission = true // 管理界面默认允许所有登录用户访问
 
   // 检查目标路由是否在允许的范围内
@@ -321,6 +348,8 @@ router.beforeEach(async (to, from, next) => {
     isAllowed = true
   } else if ((to.path === aiCourseRoutes.home || aiCourseRoutes.children.includes(to.path)) && hasAiCoursePermission) {
     isAllowed = true
+  } else if ((to.path === aiPoetryRoutes.home || aiPoetryRoutes.children.includes(to.path)) && hasAiPoetryPermission) {
+    isAllowed = true
   }
 
   if (isAllowed) {
@@ -333,4 +362,4 @@ router.beforeEach(async (to, from, next) => {
 })
 export default router;
 
-export { homeRoutes, blocklyRoutes, aiCourseRoutes, managementRoutes };
+export { homeRoutes, blocklyRoutes, aiCourseRoutes, aiPoetryRoutes, managementRoutes };

+ 7 - 6
src/views/AIPage/AIDevelop.vue

@@ -163,7 +163,7 @@
                         :cacheDataHistoryKey="course.cacheDataHistoryKey"
                      @saveProgress="handleSaveProgress"></ImageToImage>
 
-          <!--图生视频-->  
+          <!--图生视频-->
           <ImageToVideo class="contentClass" v-if="course.courseContentType === 'aiImageToVideo'" ref="aiImageToVideo"
                         :isCourse="true"
                         :cacheDataKey="course.cacheDataKey"
@@ -179,6 +179,7 @@
             :back-text="boxIconTitle"
             :is-last-course="isLastCourse"
             @dialogue-ended="handleDialogueEnded"
+            @go-back="goBack"
           />
         </template>
 
@@ -205,7 +206,7 @@
       :currentQuestion="courseConfig"
       :gradeId="gradeId"
       :typeId="typeId"
-      :courseId="course.id || ''" 
+      :courseId="course.id || ''"
       @closeQuestionDialog="closeQuestionDialog"
       @submitAnswer="handleSubmitAnswer"
       @saveProgress="handleSaveProgress"
@@ -559,12 +560,12 @@ const handlePageClick = (event) => {
   // 检查点击目标是否在弹窗内部
   const promptPopupElement = event.target.closest('.prompt-popup-content');
   const playPromptElement = event.target.closest('.play-prompt-container');
-  
+
   // 如果点击目标不在弹窗内部,则关闭弹窗
   if (!promptPopupElement && promptPopupVisible.value) {
     promptPopupVisible.value = false;
   }
-  
+
   if (!playPromptElement && playPromptVisible.value) {
     playPromptVisible.value = false;
   }
@@ -627,7 +628,7 @@ onMounted(async () => {
         // 检查并处理courseConfigList
         if (course.courseConfigList && Array.isArray(course.courseConfigList)) {
           // 过滤掉ccTime为0的配置项
-          const validConfigList = course.courseConfigList.filter(config => 
+          const validConfigList = course.courseConfigList.filter(config =>
             config.ccTime !== undefined && config.ccTime !== null && config.ccTime > 0
           )
           return {
@@ -1648,4 +1649,4 @@ $text-color: #483d8b; // 文本颜色:靛蓝色
   border-radius: rpx(15);
   overflow: auto;
 }
-</style>
+</style>

+ 7 - 2
src/views/AIPage/aiGenerate/DialogContent.vue

@@ -5,6 +5,7 @@
     :back-text="backText"
     :is-last-course="isLastCourse"
     @dialogue-ended="handleDialogueEnded"
+    @go-back="handleGoBack"
   />
 </template>
 
@@ -33,13 +34,17 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['dialogueEnded']);
+const emit = defineEmits(['dialogueEnded', 'goBack']);
 
 const handleDialogueEnded = (isLastCourse) => {
   emit('dialogueEnded', isLastCourse);
 };
+
+const handleGoBack = () => {
+  emit('goBack');
+};
 </script>
 
 <style scoped lang="scss">
 /* 原有样式已迁移到各子组件中 */
-</style>
+</style>

+ 832 - 0
src/views/AiPoetry/AIPoetryCourse.vue

@@ -0,0 +1,832 @@
+<template>
+  <div class="home-container">
+    <div class="sidebar-container">
+      <div class="sidebar-layout">
+        <div
+            class="icon-expand"
+            :style="{
+            backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
+            left: drawerVisible ? '20%' : '0'
+          }"
+            @click="toggleDrawer"
+        >
+          <span
+              class="vertical-lines"
+              :style="{
+              color: drawerVisible ? '#8a78d0' : 'white'
+            }"
+          >||</span
+          >
+        </div>
+      </div>
+
+      <transition name="drawer-slide">
+        <div class="main-content" :class="{ 'hidden': !drawerVisible }" v-if="drawerVisible">
+          <div class="drawer-box">
+            <el-row class="tac">
+              <el-col :span="12">
+                <el-menu
+                    v-if="menuInitialized"
+                    :default-active="currentActiveIndex"
+                    :default-openeds="[currentOpenedMenu]"
+                    :class="{ 'el-menu-vertical-demo': true }"
+                    unique-opened
+                >
+                  <el-sub-menu
+                      v-for="menu in menuList"
+                      :key="menu.id"
+                      :index="menu.id"
+                      @click="handleMenuClick(menu)"
+                  >
+                    <template #title>
+                      <span>{{ menu.title }}</span>
+                    </template>
+                    <el-menu-item
+                        v-for="(item, index) in menu.data"
+                        :key="menu.id + '-' + index"
+                        :index="menu.id + '-' + (index + 1).toString()"
+                        @click="goToAIExperience(item)"
+                    >
+                      {{ item.ctTypeSort }} {{ item.ctType }}
+                    </el-menu-item>
+                  </el-sub-menu>
+                </el-menu>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+      </transition>
+    </div>
+
+    <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>
+            {{ pageTitle }}
+          </div>
+          <div class="dropdown-box">
+            <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" @visible-change="handleVisibleChange">
+              <el-button type="primary">
+                {{ selectedGrade }}
+                <el-icon class="el-icon--right" v-if="!dropdownVisible"><ArrowDownBold /></el-icon>
+                <el-icon class="el-icon--right" v-else><ArrowUpBold /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu class="dropdown-menu">
+                  <el-dropdown-item
+                      v-for="item in classData"
+                      :key="item.id"
+                      :command="item.ctType"
+                  >{{ item.ctType }}</el-dropdown-item
+                  >
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </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="ctType"
+                :trigger-on-focus="false"
+                :key="searchKey"
+            >
+              <template #prefix>
+                <el-icon class="el-input__icon"><search /></el-icon>
+              </template>
+              <template #default="{ item }">
+                <div class="scrollbar">
+                  {{ item.ctTypeSort }} {{ item.ctType }}
+                </div>
+              </template>
+            </el-autocomplete>
+          </div>
+        </div>
+      </div>
+
+      <div class="box-2">
+        <div
+            class="small-box parent-mobile-course-item"
+            v-for="(course, index) in currentCourseData"
+            :key="course.id"
+        >
+          <div
+              class="nested-box"
+              :style="{
+              backgroundImage: `url(${course.ctTypeImage})`,
+              backgroundSize: 'cover'
+            }"
+              @click="goToAIExperience(course)"
+          ></div>
+          <div class="additional-text">
+            {{ course.ctTypeSort }} {{ course.ctType }}
+          </div>
+        </div>
+
+        <div class="parent-mobile-container">
+          <div
+              class="parent-mobile-item"
+              v-for="(course, index) in currentCourseData"
+              :key="course.id"
+              @click="goToAIExperience(course)"
+          >
+            <div class="parent-mobile-image">
+              <img :src="course.ctTypeImage" :alt="course.title" />
+            </div>
+            <div class="parent-mobile-content">
+              <div class="parent-mobile-title">
+                <span class="parent-mobile-index">{{ course.ctTypeSort }}</span>
+                {{ course.ctType }}
+              </div>
+              <div class="parent-mobile-desc">{{ course.ctTypeDescribe || '' }}</div>
+            </div>
+          </div>
+        </div>
+
+        <div v-if="currentCourseData.length === 0" class="empty-state" style="width: 100%; text-align: center; padding: 40px;">
+          <p>暂无课程</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, watch } from 'vue'
+import { ClassList, ClassOutline } from '@/api/class.js'
+import {
+  ArrowUpBold,
+  ArrowDownBold,
+  ArrowRightBold,
+  Expand,
+  Reading,
+  Fold,
+  Tickets
+} from '@element-plus/icons-vue'
+import { Search, ArrowLeftBold } from '@element-plus/icons-vue'
+import { Message } from '@/utils/message/Message.js'
+import { useRouter } from 'vue-router'
+import { CONFIG } from '@/utils/roleUtils.js'
+import '@/styles/parentMobileCourse.css'
+
+const router = useRouter()
+const selectedGrade = ref('')
+const isMobile = ref(false)
+const drawerVisible = ref(true)
+const dropdownVisible = ref(false)
+
+const handleVisibleChange = (visible) => {
+  dropdownVisible.value = visible
+}
+
+const poetryCourseData = ref([])
+
+const menuConfigTemp = [
+  { id: '/poetry', title: 'AI诗词课', type: '3', data: poetryCourseData }
+]
+
+const menuConfig = computed(() => {
+  return menuConfigTemp.filter(item => {
+    return roleRouteMenuSet.value.includes(item.id)
+  })
+})
+
+const menuList = computed(() => {
+  return menuConfig.value.map(menu => ({
+    ...menu,
+    data: menu.data.value
+  }))
+})
+
+const roleRouteMenuSet = computed(() => {
+  try {
+    const roleRouteMenuStr = localStorage.getItem(CONFIG.USER_ROLE_ROUTE_MENU_KEY)
+    return roleRouteMenuStr ? JSON.parse(roleRouteMenuStr) : []
+  } catch (error) {
+    console.error('Error parsing roleRouteMenuSet:', error)
+    return []
+  }
+})
+
+const currentActiveIndex = ref('/poetry-1')
+const currentOpenedMenu = ref('/poetry')
+const menuInitialized = ref(false)
+const activeMenuKey = 'aiPoetryCourseActiveMenu'
+const activeIndexKey = 'aiPoetryCourseActiveIndex'
+
+const processCourseData = (data) => {
+  return data.map((item, index) => {
+    item.ctTypeSort = index + 1
+    item.ctTypeSort = item.ctTypeSort > 9 ? item.ctTypeSort : "0" + item.ctTypeSort
+    return item
+  })
+}
+
+const saveActiveState = (menuId, index) => {
+  localStorage.setItem(activeMenuKey, menuId)
+  localStorage.setItem(activeIndexKey, index)
+}
+
+const fetchClassOutline = async (classId) => {
+  try {
+    await Promise.all(menuConfig.value.map(async (menu) => {
+      const res = await ClassOutline(classId, menu.type)
+      if (res.code === 0) {
+        menu.data.value = processCourseData(res.data)
+      }
+    }))
+  } catch (error) {
+    console.error('获取课程大纲数据失败:', error)
+  }
+}
+
+const handleMenuClick = (menu) => {
+  currentOpenedMenu.value = menu.id
+  saveActiveState(menu.id, menu.id)
+  SearchInput.value = ''
+
+  if (menu.data.value && menu.data.value.length > 0) {
+    currentActiveIndex.value = menu.id + '-1'
+  }
+}
+
+const handleGradeSelect = command => {
+  selectedGrade.value = command
+  localStorage.setItem('selectedGrade', command)
+  SearchInput.value = ''
+  const selectedItem = classData.value.find(item => item.ctType === command)
+  if (selectedItem) {
+    localStorage.setItem('selectedGradeId', selectedItem.id)
+    fetchClassOutline(selectedItem.id).then(() => {
+      const savedMenu = localStorage.getItem(activeMenuKey)
+      const savedIndex = localStorage.getItem(activeIndexKey)
+      if (savedMenu && savedIndex) {
+        currentOpenedMenu.value = savedMenu
+        currentActiveIndex.value = savedIndex
+      }
+    })
+  }
+}
+
+const checkIsMobile = () => {
+  if (typeof window !== 'undefined') {
+    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
+        window.innerWidth <= 768;
+  }
+  return false;
+}
+
+const toggleDrawer = () => {
+  drawerVisible.value = !drawerVisible.value
+}
+
+const classData = ref([])
+
+const fetchCtTypes = async () => {
+  try {
+    const response = await ClassList()
+    if (response.code === 0) {
+      classData.value = response.data
+      const savedGrade = localStorage.getItem('selectedGrade')
+      selectedGrade.value = savedGrade || (classData.value.length > 0 ? classData.value[0].ctType : '')
+      const selectedItem = classData.value.find(item => item.ctType === selectedGrade.value) || classData.value[0]
+      if (selectedItem) {
+        fetchClassOutline(selectedItem.id)
+      }
+    }
+  } catch (error) {
+    console.error('获取 ctType 数据失败:', error)
+  }
+}
+
+const pageTitle = ref('返回首页')
+
+onMounted(() => {
+  isMobile.value = checkIsMobile();
+  if (isMobile.value) {
+    drawerVisible.value = false;
+  }
+
+  fetchCtTypes()
+
+  const savedMenu = localStorage.getItem(activeMenuKey)
+  const savedIndex = localStorage.getItem(activeIndexKey)
+
+  if (savedMenu && savedIndex) {
+    currentOpenedMenu.value = savedMenu
+    currentActiveIndex.value = savedIndex
+  }
+
+  menuInitialized.value = true
+})
+
+const SearchInput = ref('')
+const searchKey = ref(Date.now())
+
+const currentCourseData = computed(() => {
+  const menu = menuConfig.value.find(m => m.id == currentOpenedMenu.value)
+  return (menu && menu.data.value) || poetryCourseData.value
+})
+
+const querySearch = (queryString, cb) => {
+  const data = currentCourseData.value
+  const results = queryString
+      ? data.filter(item => {
+        return item.ctType.toLowerCase().includes(queryString.toLowerCase()) ||
+            (item.ctTypeSort && item.ctTypeSort.toString().includes(queryString))
+      })
+      : data
+  cb(results)
+}
+
+const handleSearchSelect = item => {
+  goToAIExperience(item)
+  SearchInput.value = '';
+}
+
+const buttonVisible = ref(false)
+
+const goBack = () => {
+  router.push('/aiPoetryHome')
+}
+
+const goToAIExperience = (outlineData) => {
+  const menuId = currentOpenedMenu.value
+  const menu = menuConfig.value.find(m => m.id === menuId)
+  const currentData = menu ? menu.data.value : []
+  const index = currentData.findIndex(item => item.id === outlineData.id)
+
+  if (index !== -1) {
+    const menuIndex = menuId + '-' + (index + 1)
+    saveActiveState(menuId, menuIndex)
+  }
+
+  router.push({
+    path: '/ai-poetry-develop',
+    state: { typeId: outlineData.id, typeName: outlineData.ctType, typeSort: outlineData.ctTypeSort }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.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;
+  transition: all 0.3s ease;
+}
+
+.home-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: row;
+  background: linear-gradient(
+          to bottom,
+          #e2ddfc,
+          #f1effd
+  );
+  gap: rpx(0);
+}
+
+.sidebar-layout {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+}
+
+.main-content {
+  width: rpx(150);
+  height: 100%;
+  flex-grow: 1;
+  background: linear-gradient(to bottom, hsl(230, 100%, 21%), #8a78d0);
+  position: relative;
+  overflow-y: auto;
+  max-height: 100%;
+  transition: all 0.3s ease;
+
+  &::-webkit-scrollbar {
+    width: rpx(0);
+  }
+
+  &::-webkit-scrollbar-track {
+    background-color: rgba(255, 255, 255, 0.1);
+    border-radius: rpx(2);
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background-color: rgba(255, 255, 255, 0.3);
+    border-radius: rpx(2);
+    transition: background-color 0.3s ease;
+  }
+
+  &::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(255, 255, 255, 0.5);
+  }
+}
+
+.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% 90%, 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);
+}
+
+.content-box {
+  flex: 1;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(
+          to bottom,
+          #e2ddfc,
+          #f1effd
+  );
+}
+
+.tac .el-menu {
+  background-color: transparent;
+  border: none;
+  width: 100%;
+  margin-left: rpx(10);
+  margin-top: rpx(10);
+}
+
+.el-sub-menu ::v-deep(.el-menu){
+  background-color: transparent;
+}
+
+.el-menu-item {
+  width: rpx(130);
+  height: rpx(25);
+  margin-bottom: rpx(5);
+  border-radius: rpx(6);
+  color: white;
+  font-size: rpx(8);
+}
+
+.el-sub-menu ::v-deep(.el-sub-menu__title) {
+  width: rpx(130);
+  margin-bottom: rpx(5);
+  border-radius: rpx(6);
+  color: white;
+  font-size: rpx(9);
+  font-weight: bold;
+  padding-left: rpx(10) !important;
+  height: rpx(25);
+  line-height: rpx(30);
+  background: rgba(255, 255, 255, 0.1);
+  transition: all 0.3s ease;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.el-sub-menu ::v-deep(.el-sub-menu__title:hover) {
+  background: linear-gradient(
+          to bottom,
+          #ffd700,
+          #ffa500
+  );
+  color: black;
+  box-shadow: 0 4px 8px rgb(0, 0, 0, 0.2);
+}
+
+.el-sub-menu.is-opened ::v-deep(.el-sub-menu__title) {
+  background: linear-gradient(
+          to bottom,
+          #ffd700,
+          #ffa500
+  );
+  color: black;
+  box-shadow: 0 4px 8px rgb(0, 0, 0, 0.2);
+}
+
+.el-sub-menu ::v-deep(.el-icon) {
+  font-size: rpx(12);
+  color: white;
+}
+
+.el-sub-menu ::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-sub-menu.is-opened ::v-deep(.el-icon) {
+  color: black;
+}
+
+.el-sub-menu ::v-deep(.el-sub-menu__title:hover .el-icon) {
+  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,
+          #ffd700,
+          #ffa500
+  );
+  box-shadow: 0 4px 8px rgb(0, 0, 0, 0.2);
+  color: black;
+}
+
+.drawer-box {
+  position: absolute;
+  display: flex;
+  margin-top: rpx(10);
+  height: 100%;
+  width: 100%;
+}
+
+.box-1 {
+  width: 100%;
+  height: rpx(50);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  box-sizing: border-box;
+  font-size: rpx(16);
+}
+
+.inner-box {
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: rpx(16);
+}
+
+.left-box {
+  position: relative;
+  justify-content: left;
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: rpx(5);
+}
+
+.box-icon {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  color: black;
+  padding-left: rpx(15);
+  font-size: rpx(10);
+  cursor: pointer;
+  z-index: 999;
+}
+
+.dropdown-box {
+  height: 100%;
+  display: flex;
+  align-items: center;
+}
+
+.dropdown-box .el-button {
+  width: rpx(60);
+  height: rpx(15);
+  background-color: rgb(255, 255, 255, 0.7);
+  border: 1px white solid;
+  box-shadow: 0 4px 8px rgb(0, 0, 0, 0.3);
+  color: black;
+  border: none;
+  margin-left: rpx(10);
+  border-radius: rpx(12);
+  font-size: rpx(8);
+}
+
+.dropdown-box .el-button:hover,
+.dropdown-box .el-button:focus,
+.dropdown-box .el-button:active {
+  border: none;
+  outline: none;
+}
+
+.dropdown-menu {
+  width: rpx(100);
+  border-radius: rpx(5);
+  border: 1px white solid;
+  background-color: rgb(255, 255, 255, 0.5);
+  backdrop-filter: blur(rpx(5));
+  box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
+  margin-left: rpx(40);
+}
+
+.dropdown-menu ::v-deep(.el-dropdown-menu__item) {
+  font-size: rpx(8);
+  color: black;
+  border-radius: rpx(5);
+  width: rpx(78);
+  height: rpx(20);
+  margin-left: rpx(4);
+  margin-bottom: rpx(8);
+}
+
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:hover),
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:focus),
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:active) {
+  background: linear-gradient(
+          to bottom,
+          #fee78a,
+          #ffce1b
+  );
+}
+
+.right-box {
+  flex: 1;
+  position: relative;
+  display: flex;
+  justify-content: right;
+  align-items: center;
+}
+
+.top-right-box {
+  width: rpx(130);
+  display: flex;
+  justify-content: flex;
+}
+
+.top-right-box {
+  ::v-deep(.el-input__wrapper) {
+    height: rpx(15);
+    font-size: rpx(6);
+    background-color: rgb(255, 255, 255, 0.5);
+    border-radius: rpx(12);
+    border: white 1px solid;
+    color: #aaa5c5;
+  }
+
+  ::v-deep(.el-input__icon) {
+    color: #aaa5c5;
+  }
+
+  ::v-deep(.el-input__inner::placeholder) {
+    color: #aaa5c5;
+  }
+
+  ::v-deep(.el-input__inner) {
+    color: black;
+  }
+
+  ::v-deep(.el-input--prefix){
+    width: rpx(100);
+    text-align: right;
+  }
+}
+
+.search-input {
+  width: rpx(200);
+  height: rpx(30);
+  font-size: rpx(9);
+  border-radius: rpx(8);
+  border: 1px solid #8B4513;
+}
+
+.box-2 {
+  width: 100%;
+  flex: 1;
+  box-sizing: border-box;
+  display: flex;
+  flex-wrap: wrap;
+  cursor: pointer;
+  overflow-y: auto;
+}
+
+.box-2::-webkit-scrollbar {
+  width: rpx(2);
+}
+
+.box-2::-webkit-scrollbar-track {
+  background: transparent;
+  border-radius: rpx(3);
+}
+
+.box-2::-webkit-scrollbar-thumb {
+  background: linear-gradient(to bottom, #8B4513, #D2691E);
+  border-radius: rpx(3);
+}
+
+.box-2::-webkit-scrollbar-thumb:hover {
+  background: linear-gradient(to bottom, #8B4513, #D2691E);
+}
+
+.small-box {
+  flex: 0 0 calc(33.333% - rpx(10));
+  margin-left: rpx(10);
+  margin-top: rpx(3);
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: center;
+  color: #8B4513;
+  font-size: rpx(8);
+}
+
+.nested-box {
+  width: rpx(150);
+  height: rpx(80);
+  border-radius: rpx(10);
+  margin-top: rpx(5);
+  display: flex;
+  border: 1px solid #8B4513;
+  justify-content: center;
+  align-items: center;
+}
+
+.nested-box:hover,
+.nested-box:active {
+  box-shadow: 0 4px 8px rgba(139, 69, 19, 0.5);
+}
+
+.additional-text {
+  margin-bottom: rpx(4);
+  font-size: rpx(8);
+}
+</style>
+
+<style lang="scss">
+.el-popper__arrow {
+  display: none;
+}
+
+.el-popper.is-light,
+.el-dropdown__popper.el-popper {
+  background: transparent;
+  border: none;
+  box-shadow: none;
+}
+
+.el-dropdown__popper {
+  --el-dropdown-menuItem-hover-color: none;
+}
+</style>
+
+<style lang='scss'>
+@use 'sass:math';
+
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.el-autocomplete-suggestion .el-scrollbar__wrap{
+  margin: 0 auto;
+  background-color: rgba(255, 255, 255, 0.7);
+  border: 2px solid #8B4513;
+  border-radius: rpx(5);
+  backdrop-filter: blur(rpx(5));
+}
+
+.el-autocomplete-suggestion li{
+  color: #8B4513;
+  font-size: rpx(7);
+  padding: rpx(5) rpx(8);
+}
+
+.el-autocomplete-suggestion li:hover{
+  background: linear-gradient(
+          to bottom,
+          #ffd700,
+          #ffa500
+  );
+}
+</style>

+ 1652 - 0
src/views/AiPoetry/AIPoetryDevelop.vue

@@ -0,0 +1,1652 @@
+<template>
+  <!-- AI发展历程 -->
+  <div class="home-container" @click="handlePageClick">
+    <!-- 展开收起侧边栏 -->
+    <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>
+            {{ 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">
+        <!-- 课程标题 -->
+        <div class="small-title">
+          <span>{{ course.courseName }}</span>
+        </div>
+
+        <el-empty v-if="isDisabled"
+                  image-size="500"
+          description="您无权查看该课程!"
+          :image="isDisabledImage"
+        />
+
+        <template v-else>
+          <!-- 视频组件 -->
+          <VideoPlayer
+              v-if="course.courseContentType === 'video'"
+              :contentType="course.courseContentType"
+              :videoPath="course.courseVideoPath"
+              :courseId="course.id || ''"
+              :typeId="typeId"
+              :courseConfigList="course.courseConfigList || []"
+              :allIndices="flattenMenuItems()"
+              :currentIndex="course.key || ''"
+              @timeUpdate="handleVideoTimeUpdate"
+              @videoEnded="handleVideoEnded"
+              @switchVideo="handleSelect"
+              @saveProgress="handleSaveProgress"
+          />
+          <!-- 图片 -->
+          <ImageView v-if="course.courseContentType === 'image'"
+                     :imagePath="course.courseImagePath"
+                     altText="课程图片"
+                     :courseId="course.id"
+                     @saveProgress="handleSaveProgress"
+          ></ImageView>
+
+          <!-- PPT -->
+          <PptView v-if="course.courseContentType === 'ppt'"
+                  :pptPath="course.pptPath"
+                  :courseId="course.id"
+                  @saveProgress="handleSaveProgress"
+                  ref="pptRef"></PptView>
+
+          <!--文生文-->
+          <TextToText class="contentClass" v-if="course.courseContentType === 'aiTextToText'" ref="aiTextToText"
+                      :isCourse="true"
+                      :cacheDataKey="course.cacheDataKey"
+                      :cacheDataHistoryKey="course.cacheDataHistoryKey"
+                     @saveProgress="handleSaveProgress"></TextToText>
+
+          <!--文生图-->
+          <TextToImage class="contentClass" v-if="course.courseContentType === 'aiTextToImage'" ref="aiTextToImage"
+                       :isCourse="true"
+                       :cacheDataKey="course.cacheDataKey"
+                       :cacheDataHistoryKey="course.cacheDataHistoryKey"
+                     @saveProgress="handleSaveProgress"></TextToImage>
+
+          <!--图生图-->
+          <ImageToImage class="contentClass" v-if="course.courseContentType === 'aiImageToImage'" ref="aiImageToImage"
+                        :isCourse="true"
+                        :cacheDataKey="course.cacheDataKey"
+                        :cacheDataHistoryKey="course.cacheDataHistoryKey"
+                     @saveProgress="handleSaveProgress"></ImageToImage>
+
+          <!--图生视频-->
+          <ImageToVideo class="contentClass" v-if="course.courseContentType === 'aiImageToVideo'" ref="aiImageToVideo"
+                        :isCourse="true"
+                        :cacheDataKey="course.cacheDataKey"
+                        :cacheDataHistoryKey="course.cacheDataHistoryKey"
+                     @saveProgress="handleSaveProgress"></ImageToVideo>
+
+          <!-- 对话界面(ailab课程类型) -->
+          <DialogContent
+            v-if="course.courseContentType === 'ailab'"
+            :key="course.key"
+            :scriptRoles="scriptRoles"
+            :scriptData="course.courseContent"
+            :back-text="boxIconTitle"
+            :is-last-course="isLastCourse"
+            @dialogue-ended="handleDialogueEnded"
+            @go-back="goBack"
+          />
+        </template>
+
+        <!-- 视频切换按钮 - 始终显示 -->
+        <div class="video-switch">
+          <div class="caret-left" @click="playPreviousVideo">
+            <el-button type="warning" round :disabled="flattenMenuItems().indexOf(course.key) === 0">
+              <img :src="leftImg" alt="Left" />上一节</el-button
+            >
+          </div>
+          <div class="caret-right" @click="playNextVideo">
+            <el-button type="warning" round :disabled="flattenMenuItems().indexOf(course.key) === flattenMenuItems().length - 1"
+              >下一节<img :src="rightImg" alt="Right" />
+            </el-button>
+          </div>
+        </div>
+
+      </div>
+    </div>
+
+    <!-- 弹框组件 -->
+    <DialogComponents
+      :questionDialogVisible="questionDialogVisible"
+      :currentQuestion="courseConfig"
+      :gradeId="gradeId"
+      :typeId="typeId"
+      :courseId="course.id || ''"
+      @closeQuestionDialog="closeQuestionDialog"
+      @submitAnswer="handleSubmitAnswer"
+      @saveProgress="handleSaveProgress"
+    />
+
+    <!-- 提示弹窗组件 -->
+    <PromptPopup
+      :visible="promptPopupVisible"
+      @confirm="handlePromptConfirm"
+      @cancel="handlePromptCancel"
+    />
+
+    <!-- 播放提示组件 -->
+    <PlayPrompt
+      :visible="playPromptVisible"
+      @countdownEnd="handlePlayPromptEnd"
+      @close="handlePlayPromptClose"
+    />
+
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { Search, ArrowLeftBold } from '@element-plus/icons-vue'
+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'
+
+// 导入全局状态
+import { globalState } from '@/utils/globalState.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'
+import PromptPopup from '@/components/popup/PromptPopup.vue'
+import PlayPrompt from '@/components/popup/PlayPrompt.vue'
+
+
+// AI实验室
+import TextToText from "@/components/ai/text/TextToText.vue";
+import TextToImage from "@/components/ai/image/TextToImage.vue";
+import ImageToImage from "@/components/ai/image/ImageToImage.vue";
+import ImageToVideo from "@/components/ai/video/ImageToVideo.vue";
+import DialogContent from '@/views/AIPage/aiGenerate/DialogContent.vue';
+import {DICT_TYPE} from "@/utils/dictUtils.js";
+import {teacherList} from "@/api/teachers.js";
+
+const router = useRouter() // 获取当前路由对象
+
+// 添加抽屉显示状态
+const drawerVisible = ref(false)
+// 渲染页面标题
+const boxIconTitle = ref('')
+// 课程集合数据
+const courseList = ref([])
+//当前课程
+const course = ref({})
+// 菜单数据
+const menuItems = ref([])
+// 课程集合数据
+const videoPathMap = ref({})
+// 已观看课程ID列表
+const watchedCourseIds = ref([])
+// 试题弹框显示状态
+const questionDialogVisible = ref(false)
+// 当前显示的试题
+const courseConfig = ref({})
+// 搜索框
+const SearchInput = ref('')
+// 年级id
+const gradeId = ref('')
+// 课程大纲id
+const typeId = ref('')
+// 课程小节id
+const courseId = ref('')
+// 课程排序
+const typeSort = ref('')
+
+// 测试账号禁用视频
+const isDisabled = ref(false)
+
+// 最后一节提示弹窗显示状态
+const promptPopupVisible = ref(false)
+// 即将播放下一节提示显示状态
+const playPromptVisible = ref(false)
+
+// 计算是否是最后一节课
+const isLastCourse = computed(() => {
+  const allIndices = flattenMenuItems()
+  const currentIndexInList = allIndices.indexOf(course.value.key)
+  return currentIndexInList === allIndices.length - 1
+})
+
+// 处理 DialogContent 对话结束事件
+const handleDialogueEnded = (isLast) => {
+  if (isLast) {
+    // 最后一节课,检查课程类型
+    if (course.value.courseContentType === 'ailab') {
+      // ailab类型显示已经是最后一节课提示
+      Message().notifyWarning('已经是最后一节课', true)
+      return
+    }
+    // 其他类型显示返回提示弹窗
+    promptPopupVisible.value = true
+  } else {
+    // 不是最后一节课,显示播放提示
+    playPromptVisible.value = true
+  }
+}
+
+// 脚本角色数据
+const scriptRoles = ref([])
+
+// 保存视频进度接口
+const handleSaveProgress = async (type, progress) => {
+
+  if (!progress) {
+    return;
+  }
+  let saveProgressData = {
+    brpNjId: gradeId.value,
+    brpType: type,
+    brpProgress: progress
+  }
+  // aiCount类型不需要添加课程大纲id和课程小节id
+  if (type !== "aiCount") {
+    saveProgressData.brpCtId = typeId.value
+    saveProgressData.brpCourseId = course.value.id
+  }
+  try {
+    await saveRecord(saveProgressData)
+  } catch (error) {
+    console.error('保存视频进度失败:', error)
+  }
+}
+
+//课程小节字典
+const menuDict = ref({
+  1: '课前回顾',
+  2: '课程引入',
+  3: '知识讲解',
+  4: '趣味实操',
+  5: '课程总结'
+})
+
+// 切换抽屉显示状态的函数
+const toggleDrawer = () => {
+  drawerVisible.value = !drawerVisible.value
+}
+
+// 返回上一页
+const goBack = () => {
+  router.push('/ai-poetry-course')
+}
+
+// 菜单打开和关闭的处理函数
+const handleOpen = () => {}
+const handleClose = () => {}
+
+// 菜单选择的处理函数
+const handleSelect = index => {
+
+  // 根据索引切换视频
+  if (videoPathMap.value[index]) {
+    course.value = videoPathMap.value[index]
+    setCacheDataKey()
+    courseId.value = course.value.id
+    // 切换标题后,关闭抽屉
+    drawerVisible.value = false
+  } else {
+    //视频不存在
+    Message().notifyWarning('视频不存在!', true)
+  }
+
+  //测试账号禁用视频
+  if (disableMsg(index)) return
+}
+
+//填充缓存key
+const setCacheDataKey = () => {
+  // 设置缓存key
+  course.value.cacheDataKey = localStorage.getItem("token") + "_course_" + course.value.id
+  course.value.cacheDataHistoryKey = course.value.cacheDataKey + "_history"
+}
+
+// 展平所有菜单项索引
+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 = () => {
+  const allIndices = flattenMenuItems()
+  const currentIndexInList = allIndices.indexOf(course.value.key)
+  if (currentIndexInList > 0) {
+    const previousIndex = allIndices[currentIndexInList - 1]
+    handleSelect(previousIndex)
+  }
+}
+
+// 播放下一个视频
+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)
+  }
+}
+
+
+// 播放结束
+const handleVideoEnded = () => {
+  console.log("播放结束")
+  // 记录当前视频ID为已观看
+  if (
+    course.value &&
+    course.value.id &&
+    !watchedCourseIds.value.includes(course.value.id)
+  ) {
+    watchedCourseIds.value.push(course.value.id)
+    localStorage.setItem(
+      'watchedCourseIds',
+      JSON.stringify(watchedCourseIds.value)
+    )
+  }
+
+  // 自动播放下一个
+  // 检查是否是最后一节
+  if (courseList.value && courseList.value.length > 0) {
+    const currentIndex = courseList.value.findIndex(item => item.id === course.value.id)
+    if (currentIndex === courseList.value.length - 1) {
+      // 显示提示弹窗
+      promptPopupVisible.value = true
+    } else {
+      // 显示播放提示
+      playPromptVisible.value = true
+    }
+  }
+}
+
+// 禁用视频(提示语)
+const disableMsg = (index = course.value.key) => {
+
+  //配置了课程权限,且视频id不在权限列表中
+  isDisabled.value = videoPathMap.value[index].dataReadonly
+  if (isDisabled.value) {
+    Message().notifyWarning('您的账号并未开放此课程!', true)
+    return isDisabled.value;
+  }
+
+  return isDisabled.value;
+}
+
+// 处理视频时间更新事件
+const handleVideoTimeUpdate = ({ currentTime, progressPercentage, courseConfig: config }) => {
+  if (config) {
+    questionDialogVisible.value = true
+    courseConfig.value = config
+    // 保存试题进度
+    const saveQuestProgress = async () =>{
+      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 saveRecord({
+            brpNjId: gradeId.value,
+            brpCtId: typeId.value,
+            brpCourseConfigId: config.id,
+            brpCourseId: courseId.value,
+            brpType: 'courseQuest',
+            brpProgress: progressPercentage
+          })
+        } else {
+          console.error('无法保存试题进度: 试题id不存在')
+        }
+      }catch(error){
+        console.error(`保存试题进度失败:`, error)
+      }
+    }
+    // 调用异步函数
+    saveQuestProgress()
+  }
+}
+
+// 关闭试题弹框
+const closeQuestionDialog = () => {
+  questionDialogVisible.value = false
+}
+
+// 提交答案
+const handleSubmitAnswer = (isCorrect) => {
+  console.log("提交答案:", isCorrect)
+  // 不直接关闭弹框,子组件自己控制关闭逻辑
+}
+
+// 处理提示弹窗确定按钮
+const handlePromptConfirm = () => {
+  promptPopupVisible.value = false
+  // 返回列表页
+  goBack()
+}
+
+// 处理提示弹窗取消按钮
+const handlePromptCancel = () => {
+  promptPopupVisible.value = false
+}
+
+// 处理播放提示倒计时结束
+const handlePlayPromptEnd = () => {
+  playPromptVisible.value = false
+  // 自动播放下一个视频
+  playNextVideo();
+}
+
+// 处理播放提示关闭
+const handlePlayPromptClose = () => {
+  playPromptVisible.value = false
+}
+
+// 处理页面点击事件,关闭提示弹窗
+const handlePageClick = (event) => {
+  // 检查点击目标是否在弹窗内部
+  const promptPopupElement = event.target.closest('.prompt-popup-content');
+  const playPromptElement = event.target.closest('.play-prompt-container');
+
+  // 如果点击目标不在弹窗内部,则关闭弹窗
+  if (!promptPopupElement && promptPopupVisible.value) {
+    promptPopupVisible.value = false;
+  }
+
+  if (!playPromptElement && playPromptVisible.value) {
+    playPromptVisible.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 = []
+  const traverse = items => {
+    items.forEach(item => {
+      if (item.children) {
+        traverse(item.children)
+      } else {
+        sections.push({ title: item.title, key: item.key })
+      }
+    })
+  }
+  traverse(menuItems.value)
+  return sections
+}
+
+// 渲染 课程数据结构 以及 视频
+onMounted(async () => {
+
+   const typeIdParam = history.state?.typeId || router.currentRoute.value.query.typeId
+  if (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
+      // 初始化已观看课程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
+        //解析AI生成课脚本json数据
+        if (courseTemp.courseContentType === 'ailab') {
+          try {
+            courseTemp.courseContent = JSON.parse(courseTemp.courseContent)
+          } catch (e) {
+            console.error('解析courseContent失败:', e)
+          }
+        }
+        videoPathMap.value[menuIndex] = courseTemp
+
+        //确定默认课程
+        if (index === 0) {
+          courseId.value = courseTemp.id
+          if(!disableMsg(menuIndex)){
+            course.value = courseTemp
+            setCacheDataKey()
+          }else{
+            course.value.key = courseTemp.key
+            course.value.courseName = courseTemp.courseName
+          }
+        }
+      })
+
+    } catch (error) {
+      console.error('获取课程数据失败:', error)
+    }
+  }
+
+  const title = history.state?.typeName || router.currentRoute.value.query.typeName
+  if (title) {
+    boxIconTitle.value = String(title)
+  }
+
+  typeSort.value = history.state?.typeSort || router.currentRoute.value.query.typeSort
+  // 初始化年级ID
+  gradeId.value = globalState.initGradeId()
+
+
+  //课程小节字典
+  let menuDictStr = localStorage.getItem(DICT_TYPE.BJDX_COURSE_LABEL);
+  let menuDictJson = menuDictStr ? JSON.parse(menuDictStr) : [];
+  menuDict.value = menuDictJson.reduce((acc, item) => {
+    acc[item.value] = item.label;
+    return acc;
+  }, {});
+
+  // 初始化角色列表
+  await getRoleList();
+})
+
+/**
+ * 获取角色列表
+ * @returns {Promise<void>}
+ */
+const getRoleList = async () => {
+  try {
+    let grade = localStorage.getItem('selectedGrade')
+    grade = grade ? grade : '小学低年级'
+    // 获取小学低年级AI数据
+    const listAiRes = await teacherList({ category: grade + 'AI' })
+    const listRes = await teacherList({ category: grade })
+    const listPoetRes = await teacherList({ category: "全部-Poet" })
+
+    const listAi = listAiRes.data.list || []
+    const list = listRes.data.list || []
+    const listPoet = listPoetRes.data.list || []
+
+    listAi.push(...list)
+    listAi.push(...listPoet)
+
+    scriptRoles.value = listAi
+  } catch (error) {
+    console.error('获取角色数据失败:', error)
+  }
+}
+
+onBeforeUnmount(() => {
+  // 组件卸载时的清理
+})
+</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;
+}
+
+.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);
+}
+
+/* 添加过渡样式 */
+.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;
+  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;
+  left: 0;
+  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 {
+  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;
+  gap: rpx(5);
+  padding: rpx(5) rpx(10);
+  background-color: rgba(255, 255, 255, 80%);
+  border-radius: rpx(30);
+  backdrop-filter: blur(10px);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-size: rpx(8);
+  color: #333;
+  font-weight: 900;
+  width: fit-content;
+  margin-left: rpx(15);
+}
+
+.box-icon:hover {
+  background-color: rgba(255, 255, 255, 90%);
+  transform: translateX(-3px);
+}
+
+.left-icon {
+  font-size: rpx(10);
+}
+// .box-icon .left-icon {
+//   margin-left: rpx(10);
+//   margin-right: rpx(5); // 设置图标和文字之间的间距 ;
+// }
+
+.left-box span {
+  position: absolute;
+  font-size: rpx(11); // 默认字体大小
+  color: white;
+}
+
+.right-box {
+  flex: 1;
+  position: relative; // 添加相对定位;
+}
+.top-right-box {
+  position: absolute; // 添加绝对定位
+  // margin-top: rpx(9); // 调整上边距离
+  margin-left: rpx(260); // 调整右边距离
+  width: rpx(100); // 设置盒子宽度,可按需调整
+  // height: 60px; // 设置盒子高度,可按需调整
+  margin-right: rpx(20);
+  display: flex;
+  justify-content: flex-end;
+}
+.top-right-box {
+  ::v-deep(.el-input__wrapper) {
+    height: rpx(15);
+    font-size: rpx(6);
+    background-color: rgb(255, 255, 255, 0.5);
+    border-radius: rpx(12);
+    border: white 1px solid;
+    color: white;
+  }
+  ::v-deep(.el-input__icon) {
+    color: white; // 设置输入框图标颜色为白色
+  }
+  // 添加占位符样式
+  ::v-deep(.el-input__inner::placeholder) {
+    color: white;
+  }
+  // 添加输入框文字颜色样式
+  ::v-deep(.el-input__inner) {
+    color: black;
+  }
+}
+// 搜索框
+.search-input {
+  width: rpx(100);
+  height: rpx(15);
+  font-size: rpx(7);
+}
+.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(20);
+  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;
+  // align-items: center;
+
+  .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);
+    // box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.2);
+  }
+
+  .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%;
+    // box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
+  }
+}
+
+// 用户输入框样式
+.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;
+  // padding: 5px 10px;
+
+  .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);
+    }
+  }
+}
+
+</style>
+
+<style lang="scss">
+// 搜索下拉框样式
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+/* 消除小三角 */
+.el-popper__arrow {
+  display: none;
+}
+.el-popper.is-light,
+.el-dropdown__popper.el-popper {
+  background: transparent;
+  border: none;
+  box-shadow: none;
+}
+.el-dropdown__popper {
+  --el-dropdown-menuItem-hover-color: none;
+}
+.el-autocomplete-suggestion .el-scrollbar__wrap {
+  margin: 0 auto;
+  background-color: rgba(255, 255, 255, 0.7);
+  border: 2px solid white;
+  border-radius: rpx(5);
+  backdrop-filter: blur(rpx(5));
+}
+.el-autocomplete-suggestion li {
+  color: black;
+  font-size: rpx(7);
+  padding: rpx(5) rpx(8); // 调整下拉项内边距
+}
+.el-autocomplete-suggestion li:hover {
+  background: linear-gradient(to bottom, #ffefb0, #ffcc00);
+}
+</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: 80vh;
+  margin: 0 auto;
+  border-radius: rpx(15);
+  overflow: auto;
+}
+</style>

+ 523 - 0
src/views/AiPoetry/PoetryHome.vue

@@ -0,0 +1,523 @@
+<template>
+  <div class="home-container">
+    <div class="box-1">
+      <div class="inner-box left-box">
+        <div class="box-icon" @click="goToManagementInterface">
+          <div class="app-header">
+            <img :src="LogoImage" alt="Logo" class="app-logo" />
+            <el-icon class="left-icon"><HomeFilled /></el-icon>
+            <span>{{ platformTitle }}</span>
+          </div>
+        </div>
+        <div class="dropdown-box">
+          <el-dropdown v-model="selectedGrade" @command="handleGradeSelect" @visible-change="handleVisibleChange" popper-class="no-arrow-dropdown">
+            <el-button type="primary">
+              {{ selectedGrade }}
+              <el-icon class="el-icon--right" v-if="!dropdownVisible"><ArrowDownBold /></el-icon>
+              <el-icon class="el-icon--right" v-else><ArrowUpBold /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu class="dropdown-menu">
+                <el-dropdown-item
+                  v-for="item in classData"
+                  :key="item.id"
+                  :command="item.ctType"
+                  >{{ item.ctType }}</el-dropdown-item
+                >
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </div>
+      <div class="inner-box right-box">
+        <div class="top-right-box">
+          <el-button
+            v-for="button in buttonConfigs"
+            :key="button.name"
+            round
+            class="top-right-btn"
+            :class="{ 'is-active': selectedButton === button.name }"
+            @click="handleButtonClick(button)"
+            >{{ button.name }}</el-button
+          >
+          <UserInfoPopover />
+        </div>
+      </div>
+    </div>
+    <div class="box-2">
+      <div
+        class="left-box-in-box2"
+        @click="goToAIPoetryCourse"
+        :style="{ backgroundImage: `url(${indexImages[0]})` }"
+      >
+        <span style="background-color: #77b7db78;
+              margin: 20px;
+              padding: 10px;
+              border-radius: 27px;">
+          AI诗词课
+        </span>
+      </div>
+      <div
+        class="center-box-in-box2"
+        @click="goToAILab()"
+        :style="{ backgroundImage: `url(${indexImages[1]})` }"
+      >
+        <span>AI实验室</span>
+      </div>
+      <div class="right-box-in-box2">
+        <div
+          class="top-sub-box"
+          :style="{ backgroundImage: `url(${indexImages[2]})` }"
+           @click="goToEvaluation"
+        >
+          <span>能力测评</span>
+        </div>
+        <div
+          class="bottom-sub-box"
+          :style="{ backgroundImage: `url(${indexImages[3]})` }"
+          @click="goToPersonalized"
+        >
+          <span>评估报告</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch } from 'vue'
+import { useRouter } from 'vue-router'
+import { ClassList } from '@/api/class.js'
+import {ArrowUpBold, ArrowDownBold, HomeFilled} from '@element-plus/icons-vue'
+import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
+import LogoImage from '@/assets/images/logo.png'
+
+import poetryImg from '@/assets/aiPoetry/poetry.png'
+import roomImg from '@/assets/images/room.png'
+import testImg from '@/assets/images/test.png'
+import studyImg from '@/assets/images/study.png'
+
+import {logoutLogic, removeLocalStorageKey} from '@/utils/loginUtils.js'
+import {aiCourseRoutes, blocklyRoutes, homeRoutes, aiPoetryRoutes} from "@/router/index.js";
+
+const platformTitle = ref(import.meta.env.VITE_APP_TITLE)
+
+const updatePlatformTitle = () => {
+  platformTitle.value = localStorage.getItem('tenantName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT
+}
+
+const router = useRouter()
+
+const handleButtonClick = (button) => {
+  selectedButton.value = button.name
+  router.push(button.route)
+}
+
+const buttonConfigs = ref([
+  // { name: 'AI编程课', route: blocklyRoutes.home },
+])
+
+const selectedButton = ref('')
+
+const indexImages = ref([poetryImg, roomImg, testImg, studyImg])
+
+const goToAIPoetryCourse = () => {
+  router.push('/ai-poetry-course')
+}
+
+const goToManagementInterface = () => {
+  router.push('/management-interface')
+}
+
+const goToAILab = () => {
+  router.push({
+    path: '/ai-laboratory',
+    state: { grade: selectedGrade.value }
+  })
+}
+
+const goToEvaluation = () =>{
+  router.push({
+   path:'/evaluation'
+ })
+}
+
+const goToPersonalized = () =>{
+  router.push({
+    path:'/personalized'
+  })
+}
+
+const selectedGrade = ref(localStorage.getItem('selectedGrade') || '')
+const selectedGradeId = ref(localStorage.getItem('selectedGradeId') || '')
+const dropdownVisible = ref(false)
+const classData = ref([])
+
+const handleVisibleChange = (visible) => {
+  dropdownVisible.value = visible
+}
+
+const fetchCtTypes = async () => {
+  try {
+    const response = await ClassList()
+    if (response.code === 0) {
+      classData.value = response.data
+     if (classData.value.length > 0 && !selectedGrade.value) {
+        selectedGrade.value = classData.value[0].ctType
+        selectedGradeId.value = classData.value[0].id
+        localStorage.setItem('selectedGrade', selectedGrade.value)
+        localStorage.setItem('selectedGradeId', selectedGradeId.value)
+      }
+    }
+  } catch (error) {
+    console.error('获取 ctType 数据失败:', error)
+  }
+}
+
+watch(selectedGrade, (newValue) => {
+  if (newValue) {
+    localStorage.setItem('selectedGrade', newValue)
+    const selectedItem = classData.value.find(item => item.ctType === newValue)
+    if (selectedItem) {
+      selectedGradeId.value = selectedItem.id
+      localStorage.setItem('selectedGradeId', selectedGradeId.value)
+    }
+  }
+})
+
+watch(selectedGradeId, (newValue) => {
+  if (newValue) {
+    localStorage.setItem('selectedGradeId', newValue)
+  }
+})
+
+const handleGradeSelect = (command) => {
+  selectedGrade.value = command
+  const selectedItem = classData.value.find(item => item.ctType === command)
+  if (selectedItem) {
+    selectedGradeId.value = selectedItem.id
+  }
+}
+
+onMounted(() => {
+  fetchCtTypes()
+  updatePlatformTitle()
+  window.addEventListener('storage', (e) => {
+    if (e.key === 'tenantName') {
+      updatePlatformTitle()
+    }
+  })
+
+  removeLocalStorageKey(localStorage.getItem("token") + "_ai_")
+  removeLocalStorageKey(localStorage.getItem('token') + "_course_")
+  removeLocalStorageKey(localStorage.getItem("token") + "_blockly_")
+  removeLocalStorageKey(localStorage.getItem("token") + "_aiCourse_")
+})
+
+window.updateTenantName = (newName) => {
+  localStorage.setItem('tenantName', newName)
+  updatePlatformTitle()
+}
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.home-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(to bottom, #001169, #8a78d0);
+  display: flex;
+  flex-direction: column;
+  gap: rpx(0);
+}
+
+.box-1 {
+  width: 100%;
+  min-height: rpx(50);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  box-sizing: border-box;
+  font-size: rpx(16);
+}
+
+.box-2 {
+  width: 90%;
+  margin: auto;
+  flex: 1;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-sizing: border-box;
+  cursor: pointer;
+}
+
+.box-2 span {
+  padding: rpx(10) 0 0 rpx(10);
+  font-size: rpx(12);
+  color: white;
+}
+
+.left-box-in-box2,
+.center-box-in-box2 {
+  background-color: rgba(133, 135, 176, 0.5);
+  border-radius: rpx(30);
+  flex: 1;
+  margin: 0 rpx(10);
+  height: 85%;
+  display: flex;
+  justify-content: flex-start;
+  align-items: flex-start;
+  background-origin: border-box;
+  background-clip: padding-box;
+}
+
+.left-box-in-box2:hover,
+.left-box-in-box2:active,
+.center-box-in-box2:hover,
+.center-box-in-box2:active {
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+.left-box-in-box2,
+.center-box-in-box2,
+.top-sub-box,
+.bottom-sub-box {
+  background-repeat: no-repeat;
+  background-size: cover;
+  background-position: center;
+}
+
+.right-box-in-box2 {
+  flex: 1;
+  margin: 0 rpx(10);
+  height: 85%;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  gap: rpx(10);
+}
+
+.top-sub-box,
+.bottom-sub-box {
+  background-color: rgba(133, 135, 176, 0.5);
+  flex: 1;
+  display: flex;
+  border-radius: rpx(20);
+  width: 100%;
+}
+
+.top-sub-box:hover,
+.top-sub-box:active,
+.bottom-sub-box:hover,
+.bottom-sub-box:active {
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+.inner-box {
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: rpx(16);
+}
+
+.inner-box.left-box {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 rpx(20);
+}
+
+.left-icon{
+  font-size: rpx(14);
+  color: white;
+  cursor: pointer;
+}
+
+.app-header {
+  width: 100%;
+  height: rpx(30);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: rpx(5);
+}
+
+.app-logo {
+  width: rpx(20);
+  height: rpx(20);
+  object-fit: contain;
+}
+
+.app-header span {
+  color: white;
+  font-size: rpx(11);
+  letter-spacing: rpx(1);
+  display: flex;
+  align-items: center;
+  height: 100%;
+}
+
+.left-box span {
+  position: static;
+  margin-top: 0;
+  margin-left: 0;
+  margin-right: rpx(10);
+  font-size: rpx(11);
+  color: white;
+  max-width: rpx(200);
+  white-space: normal;
+  line-height: rpx(16);
+  text-align: left;
+}
+
+.box-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: rpx(5);
+  padding: rpx(3) rpx(5);
+  margin-right: rpx(5);
+  border-radius: rpx(30);
+  backdrop-filter: blur(10px);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-size: rpx(14);
+  color: white;
+}
+
+.right-box {
+  flex: 1;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  margin-right: rpx(60);
+}
+
+.top-right-box {
+  width: 100%;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  flex-wrap: wrap;
+  cursor: pointer;
+}
+
+.top-right-btn {
+  width: rpx(50);
+  height: rpx(15);
+  margin: rpx(10) rpx(5) 0 0;
+  background-color: transparent;
+  color: white;
+  border: none;
+  font-size: rpx(8);
+  outline: none;
+}
+
+.top-right-btn:hover {
+  background-color: rgba(255, 255, 255, 0.7);
+  border: 1px white solid;
+  color: #8B4513;
+  outline: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+.dropdown-box {
+  align-items: center;
+  margin-top: 0;
+  display: flex;
+}
+
+.dropdown-box .el-button {
+  width: rpx(60);
+  height: rpx(15);
+  background-color: rgba(255, 255, 255, 0.7);
+  border: 1px white solid;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+  color: #8B4513;
+  border-radius: rpx(12);
+  font-size: rpx(8);
+}
+
+.dropdown-box .el-button:hover,
+.dropdown-box .el-button:focus,
+.dropdown-box .el-button:active {
+  border: none;
+  outline: none;
+}
+
+.dropdown-menu {
+  width: rpx(100);
+  border-radius: rpx(5);
+  border: 1px white solid;
+  background-color: rgba(255, 255, 255, 0.5);
+  backdrop-filter: blur(rpx(5));
+  box-shadow: 0 4px 8px rgba(139, 69, 19, 0.1);
+  margin-left: rpx(40);
+}
+
+.el-scrollbar__view .el-dropdown__list{
+  background-color: transparent;
+}
+
+.dropdown-menu ::v-deep(.el-dropdown-menu__item) {
+  font-size: rpx(8);
+  color: #8B4513;
+  border-radius: rpx(5);
+  width: rpx(78);
+  height: rpx(20);
+  margin-left: rpx(4);
+  margin-bottom: rpx(8);
+}
+
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:hover),
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:focus),
+.dropdown-menu ::v-deep(.el-dropdown-menu__item:active) {
+  background: linear-gradient(
+    to bottom,
+    #ffd700,
+    #ffa500
+  );
+  box-shadow: 0 4px 8px rgba(139, 69, 19, 0.1);
+}
+</style>
+
+<style lang="scss">
+.no-arrow-dropdown .el-popper__arrow{
+  display: none;
+}
+
+.el-popper.is-light,
+.el-dropdown__popper.el-popper{
+  background: transparent;
+  border: none;
+  box-shadow: none;
+}
+
+.el-dropdown__popper{
+  --el-dropdown-menuItem-hover-color: none;
+}
+
+.user-name-box:focus,
+.user-name-box:focus-within,
+.user-name-box:hover{
+  outline: none;
+  box-shadow: none;
+}
+
+.el-dropdown .el-dropdown__trigger:focus{
+  outline: none;
+  box-shadow: none;
+}
+</style>

+ 38 - 22
src/views/ManagementInterface.vue

@@ -15,52 +15,60 @@
     </div>
     <div class="main-content">
       <div class="inner-content">
-        <div 
-          class="small-box" 
+        <div
+          class="small-box"
           v-if="hasAiCoursePermission"
-          @click="navigateToHome(aiCourseRoutes.home)" 
+          @click="navigateToHome(aiCourseRoutes.home)"
           :style="{ backgroundImage: `url(${enlightenment})` }"
         >
           <div class="box-logout-btn">AI启蒙课<el-icon class="arrow-icon"><DArrowRight /></el-icon></div>
         </div>
-        <div 
-          class="small-box" 
+        <div
+          class="small-box"
           v-if="hasHomePermission"
-          @click="navigateToHome(homeRoutes.home)" 
+          @click="navigateToHome(homeRoutes.home)"
           :style="{ backgroundImage: `url(${programming})` }"
         >
           <div class="box-logout-btn" >AI通识课<el-icon class="arrow-icon"><DArrowRight /></el-icon></div>
         </div>
-        <div 
-          class="small-box" 
+        <div
+          class="small-box"
           v-if="hasBlocklyPermission"
-          @click="navigateToHome(blocklyRoutes.home)" 
+          @click="navigateToHome(blocklyRoutes.home)"
           :style="{ backgroundImage: `url(${general})` }"
         >
           <div class="box-logout-btn">AI编程课<el-icon class="arrow-icon"><DArrowRight /></el-icon></div>
         </div>
+        <div
+          class="small-box"
+          v-if="hasAiPoetryPermission"
+          @click="navigateToHome(aiPoetryRoutes.home)"
+          :style="{ backgroundImage: `url(${poetry})` }"
+        >
+          <div class="box-logout-btn">AI诗词课<el-icon class="arrow-icon"><DArrowRight /></el-icon></div>
+        </div>
       </div>
-      
+
       <!-- <div class="home-buttons">
-        <el-button 
-          v-if="hasHomePermission" 
-          type="primary" 
+        <el-button
+          v-if="hasHomePermission"
+          type="primary"
           @click="navigateToHome(homeRoutes.home)"
           class="home-button"
         >
           通识课首页
         </el-button>
-        <el-button 
-          v-if="hasBlocklyPermission" 
-          type="success" 
+        <el-button
+          v-if="hasBlocklyPermission"
+          type="success"
           @click="navigateToHome(blocklyRoutes.home)"
           class="home-button"
         >
           Blockly编程课首页
         </el-button>
-        <el-button 
-          v-if="hasAiCoursePermission" 
-          type="warning" 
+        <el-button
+          v-if="hasAiCoursePermission"
+          type="warning"
           @click="navigateToHome(aiCourseRoutes.home)"
           class="home-button"
         >
@@ -75,7 +83,7 @@
 import { ref, onMounted, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import UserInfoPopover from '@/components/user/UserInfoPopover.vue'
-import { homeRoutes, blocklyRoutes, aiCourseRoutes, managementRoutes } from '@/router'
+import { homeRoutes, blocklyRoutes, aiCourseRoutes, aiPoetryRoutes, managementRoutes } from '@/router'
 import { CONFIG } from '@/utils/roleUtils.js'
 import { Message } from '@/utils/message/Message.js'
 import { DArrowRight } from '@element-plus/icons-vue'
@@ -89,6 +97,8 @@ import enlightenment from '@/assets/manage/enlightenment.png'
 import programming from '@/assets/manage/programming02.png'
 // 通识课
 import general from '@/assets/manage/general02.png'
+// 诗词课
+import poetry from '@/assets/manage/poetry.png'
 
 // 平台标题响应式变量
 const platformTitle = ref(import.meta.env.VITE_APP_TITLE)
@@ -115,6 +125,12 @@ const hasAiCoursePermission = computed(() => {
   return false
  })
 
+const hasAiPoetryPermission = computed(() => {
+  const roleRouteStr = localStorage.getItem(CONFIG.USER_ROLE_ROUTE_KEY)
+  const roleRouteSet = roleRouteStr ? JSON.parse(roleRouteStr) : []
+  return roleRouteSet.includes(aiPoetryRoutes.role)
+ })
+
 // 更新平台标题
 const updatePlatformTitle = () => {
   platformTitle.value = localStorage.getItem('tenantName') || import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT
@@ -254,7 +270,7 @@ window.updateTenantName = (newName) => {
     justify-content: center;
     flex-wrap: wrap;
   }
-  
+
   .home-button {
     width: rpx(200);
     margin: 0 rpx(10);
@@ -363,4 +379,4 @@ window.updateTenantName = (newName) => {
   outline: none;
   box-shadow: none;
 }
-</style>
+</style>