فهرست منبع

增加自主学习组件

丸子 1 ماه پیش
والد
کامیت
36818973f3
3فایلهای تغییر یافته به همراه248 افزوده شده و 85 حذف شده
  1. 176 0
      src/components/study/SelfDirectedLearning.vue
  2. 59 84
      src/views/AIPage/AIGeneralCourse.vue
  3. 13 1
      src/views/AIPage/aiGenerate/DialogContent.vue

+ 176 - 0
src/components/study/SelfDirectedLearning.vue

@@ -0,0 +1,176 @@
+<!-- AI自主学习 -->
+<template>
+  <div class="self-directed-learning">
+    <div class="title-section">
+      <h3>和AI一起学你想学!</h3>
+    </div>
+    <div class="input-section">
+      <div class="dialogue-card user-input-card">
+        <div class="dialogue-content">
+          <textarea
+            v-model="prompt"
+            class="user-input-textarea"
+            placeholder=""
+            @keyup.enter.exact="doSendMessage(prompt.trim())"
+          ></textarea>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+
+// 发送消息输入框
+const prompt = ref(""); // prompt
+
+/** 处理来自 keydown 的发送消息 */
+const handleSendByKeydown = async (event) => {
+  const content = prompt.value?.trim();
+  if (event.key === "Enter") {
+    if (event.shiftKey) {
+      // 插入换行
+      prompt.value += "\r\n";
+      event.preventDefault(); // 防止默认的换行行为
+    } else {
+      // 发送消息
+      await doSendMessage(content);
+      event.preventDefault(); // 防止默认的提交行为
+    }
+  }
+};
+
+/** 真正执行【发送】消息操作 */
+const doSendMessage = async (content) => {
+  // 校验
+  if (content.length < 1) {
+    console.error("发送失败,原因:内容为空!");
+    return;
+  }
+
+  // 清空输入框
+  prompt.value = "";
+
+  // 这里可以添加发送消息的逻辑
+  console.log("发送消息:", content);
+};
+</script>
+
+<style scoped lang="scss">
+@use "sass:math";
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+
+.self-directed-learning {
+  width: 100%;
+  // min-height: rpx(100); /* 设置最小高度 */
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1; /* 允许组件在容器中生长 */
+}
+
+.title-section {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin: rpx(0) 0;
+
+  h3 {
+    font-size: rpx(12);
+    color: black;
+    font-weight: bold;
+  }
+}
+
+.input-section {
+  display: flex;
+  justify-content: center;
+  padding: rpx(10);
+}
+
+.dialogue-card {
+  background-color: #f1f0ff;
+  border: rpx(1) solid #a7a4ed;
+  border-radius: rpx(6);
+  padding: rpx(8);
+  max-width: 50%;
+  min-width: rpx(200);
+  // box-shadow: 0 rpx(3.5) rpx(10) rgba(0, 0, 0, 0.15);
+  width: auto;
+  display: inline-block;
+  z-index: 2;
+}
+
+.user-input-card {
+  max-width: 80%;
+  animation: dialogueEnterCenter 0.6s ease forwards;
+}
+
+.user-input-card::before {
+  display: none;
+}
+
+@keyframes dialogueEnterCenter {
+  from {
+    opacity: 0;
+    transform: translateY(rpx(20));
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.dialogue-content {
+  font-size: rpx(12);
+  line-height: 1.2;
+  color: #333;
+  text-align: left;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+}
+
+.user-input-textarea {
+  width: 95%;
+  min-height: rpx(50);
+  max-height: rpx(150);
+  border: none;
+  outline: none;
+  background: transparent;
+  font-size: rpx(10);
+  line-height: 1.2;
+  color: #333;
+  resize: none;
+  font-family: inherit;
+  overflow-y: auto;
+  text-align: left;
+  padding: rpx(5) 0;
+}
+
+/* 滚动条样式 */
+.user-input-textarea::-webkit-scrollbar {
+  width: rpx(0);
+}
+
+.user-input-textarea::-webkit-scrollbar-track {
+  background: rgba(0, 0, 0, 0.05);
+  border-radius: rpx(3);
+}
+
+.user-input-textarea::-webkit-scrollbar-thumb {
+  background: rgba(64, 158, 255, 0.5);
+  border-radius: rpx(3);
+}
+
+.user-input-textarea::-webkit-scrollbar-thumb:hover {
+  background: rgba(64, 158, 255, 0.8);
+}
+
+
+</style>

+ 59 - 84
src/views/AIPage/AIGeneralCourse.vue

@@ -8,7 +8,7 @@
           class="icon-expand"
           :style="{
             backgroundColor: drawerVisible ? '#44449c' : '#7F70C840',
-            left: drawerVisible ? '18%' : '0'
+            left: drawerVisible ? '20%' : '0'
           }"
           @click="toggleDrawer"
         >
@@ -118,6 +118,9 @@
       </div>
 
       <div class="box-2">
+        <!-- 自主学习组件,只在AI自主学习时显示 -->
+        <SelfDirectedLearning v-if="currentOpenedMenu === 'selfstudy'" />
+        
         <div
           class="small-box"
           v-for="(outlineData, index) in currentCourseData"
@@ -159,6 +162,8 @@ import { ElAutocomplete } from 'element-plus' // 导入ElAutocomplete组件
 import { Message } from '@/utils/message/Message.js'
 import { useRouter } from 'vue-router'
 import teachingImg from '@/assets/icon/teaching.png'
+// 导入自主学习组件
+import SelfDirectedLearning from '@/components/study/SelfDirectedLearning.vue'
 
 const router = useRouter() // 获取当前路由对象
 // 下拉菜单选中项
@@ -171,16 +176,21 @@ const dropdownVisible = ref(false)
 const handleVisibleChange = (visible) => {
   dropdownVisible.value = visible
 }
-// 通识课
-const classOutlineData = ref([])
-// 实操课
-const practicalCourseData = ref([])
-// AI生成课
-const aiGeneratedCourseData = ref([])
-// 状态变量,跟踪当前显示的是通识课、实操课还是AI生成课
-const showPracticalCourse = ref(false)
-// 状态变量,跟踪当前显示的是实操课还是AI生成课
-const showAICourse = ref(false)
+// 课程数据管理
+const courseData = ref({
+  general: [], // 通识课
+  practical: [], // 实操课
+  ai: [], // AI场景教学
+  selfstudy: [] // AI自主学习
+})
+
+// 菜单配置
+const menuConfig = [
+  { id: 'general', title: '通识课', type: '1', isPractical: false, isAICourse: false },
+  { id: 'practical', title: '实操课', type: '2', isPractical: true, isAICourse: false },
+  { id: 'ai', title: 'AI场景教学', type: '3', isPractical: false, isAICourse: true },
+  { id: 'selfstudy', title: 'AI自主学习', type: '1', isPractical: false, isAICourse: true }
+]
 // 当前选中的菜单索引
 const currentActiveIndex = ref('general-1')
 // 当前打开的菜单
@@ -190,29 +200,12 @@ const activeMenuKey = 'aiGeneralCourseActiveMenu'
 const activeIndexKey = 'aiGeneralCourseActiveIndex'
 
 // 菜单列表计算属性
-const menuList = computed(() => [
-  {
-    id: 'general',
-    title: '通识课',
-    data: classOutlineData.value,
-    isPractical: false,
-    isAICourse: false
-  },
-  {
-    id: 'practical',
-    title: '实操课',
-    data: practicalCourseData.value,
-    isPractical: true,
-    isAICourse: false
-  },
-  {
-    id: 'ai',
-    title: 'AI场景教学',
-    data: aiGeneratedCourseData.value,
-    isPractical: false,
-    isAICourse: true
-  }
-])
+const menuList = computed(() => {
+  return menuConfig.map(menu => ({
+    ...menu,
+    data: courseData.value[menu.id]
+  }))
+})
 
 // 处理课程数据,添加序号
 const processCourseData = (data) => {
@@ -232,18 +225,11 @@ const saveActiveState = (menuId, index) => {
 // 统一函数来获取课程大纲数据 通识课/实操课/AI生成课
 const fetchClassOutline = async (classId) => {
   try {
-    // 课程类型配置
-    const courseTypes = [
-      { type: "1", dataRef: classOutlineData },
-      { type: "2", dataRef: practicalCourseData },
-      { type: "3", dataRef: aiGeneratedCourseData }
-    ]
     // 并行获取所有课程类型数据
-    await Promise.all(courseTypes.map(async (courseType) => {
-      const res = await ClassOutline(classId, courseType.type)
-      console.log(res)
+    await Promise.all(menuConfig.map(async (menu) => {
+      const res = await ClassOutline(classId, menu.type)
       if (res.code === 0) {
-        courseType.dataRef.value = processCourseData(res.data)
+        courseData.value[menu.id] = processCourseData(res.data)
       }
     }))
   } catch (error) {
@@ -332,10 +318,6 @@ onMounted(() => {
   if (savedMenu && savedIndex) {
     currentOpenedMenu.value = savedMenu
     currentActiveIndex.value = savedIndex
-    
-    // 根据保存的菜单类型设置对应的显示状态
-    showPracticalCourse.value = savedMenu === 'practical'
-    showAICourse.value = savedMenu === 'ai'
   }
   
   // const title = history.state?.title
@@ -351,9 +333,7 @@ const SearchInput = ref('')
 const searchKey = ref(Date.now())
 // 当前显示的课程数据
 const currentCourseData = computed(() => {
-  if (showAICourse.value) return aiGeneratedCourseData.value
-  if (showPracticalCourse.value) return practicalCourseData.value
-  return classOutlineData.value
+  return courseData.value[currentOpenedMenu.value] || courseData.value.general
 })
 
 // 搜索建议查询方法
@@ -399,16 +379,10 @@ const goBack = () => {
 
 const goToAIExperience = outlineData => {
   // 确定当前课程所属的菜单类型
-  let menuId = 'general'
-  if (showPracticalCourse.value) {
-    menuId = 'practical'
-  } else if (showAICourse.value) {
-    menuId = 'ai'
-  }
+  const menuId = currentOpenedMenu.value
   
   // 找到当前课程在对应菜单中的索引
-  const currentData = showPracticalCourse.value ? practicalCourseData.value : 
-                     showAICourse.value ? aiGeneratedCourseData.value : classOutlineData.value
+  const currentData = courseData.value[menuId]
   const index = currentData.findIndex(item => item.id === outlineData.id)
   
   if (index !== -1) {
@@ -469,30 +443,31 @@ const goToAIExperience = outlineData => {
   background-color: saddlebrown;
 }
 .main-content {
-  width: rpx(135);
-  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); // 鼠标悬停时的滑块颜色
+    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);
@@ -537,7 +512,7 @@ const goToAIExperience = outlineData => {
   background-color: transparent;
 }
 .el-menu-item {
-  width: rpx(115);
+  width: rpx(130);
   height: rpx(25);
   margin-bottom: rpx(5);
   border-radius: rpx(6);
@@ -546,7 +521,7 @@ const goToAIExperience = outlineData => {
 }
 
 .el-sub-menu ::v-deep(.el-sub-menu__title) {
-  width: rpx(115);
+  width: rpx(130);
   margin-bottom: rpx(5);
   border-radius: rpx(6);
   color: white;

+ 13 - 1
src/views/AIPage/aiGenerate/DialogContent.vue

@@ -1348,6 +1348,7 @@ onUnmounted(() => {
   &.recording {
     &::before {
       animation: pulse 2s infinite;
+      border-color: rgba(255, 80, 80, 0.6);
     }
 
     &::after {
@@ -1356,9 +1357,20 @@ onUnmounted(() => {
       width: 85%;
       height: 85%;
       border-radius: 50%;
-      border: rpx(2) solid rgba(80, 190, 240, 0.4);
+      border: rpx(2) solid rgba(255, 80, 80, 0.4);
       animation: pulse 2s infinite 0.5s;
     }
+
+    :deep(.voice-input-container) {
+      .speech-btn {
+        background: linear-gradient(135deg, #FFA0A0, #FF5050);
+        border-color: rgba(192, 0, 0, 1);
+
+        .el-icon {
+          color: #BE0000;
+        }
+      }
+    }
   }
 
   // 占位符样式