Interface.vue 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  1. <template>
  2. <!-- 编程课程视频页面 -->
  3. <div class="home-container">
  4. <div class="content-box">
  5. <div class="box-1">
  6. <div class="inner-box left-box">
  7. <div class="box-icon" @click="emit('closeVideo')">
  8. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  9. 返回
  10. </div>
  11. </div>
  12. </div>
  13. <div class="box-2">
  14. <!-- 课程标题 -->
  15. <div class="small-title">
  16. <span>{{ course.courseName }}</span>
  17. </div>
  18. <el-empty v-if="isDisabled"
  19. image-size="500"
  20. description="您无权查看该课程!"
  21. :image="isDisabledImage"
  22. />
  23. <template v-else>
  24. <!-- 视频组件 -->
  25. <VideoPlayer
  26. v-if="course.courseContentType === 'video'"
  27. :contentType="course.courseContentType"
  28. :videoPath="course.courseVideoPath"
  29. :courseId="course.id || ''"
  30. :typeId="typeId"
  31. :courseConfigList="course.courseConfigList || []"
  32. :allIndices="flattenMenuItems()"
  33. :currentIndex="course.key || ''"
  34. @timeUpdate="handleVideoTimeUpdate"
  35. @videoEnded="handleVideoEnded"
  36. @switchVideo="switchVideo"
  37. />
  38. <!-- 图片 -->
  39. <ImageView v-if="course.courseContentType === 'image'" :imagePath="course.courseImagePath" altText="课程图片"></ImageView>
  40. <!-- PPT -->
  41. <PptView v-if="course.courseContentType === 'ppt'" :pptPath="course.pptPath" ref="pptRef"></PptView>
  42. <!--文生文-->
  43. <TextToText class="contentClass" v-if="course.courseContentType === 'aiTextToText'" ref="aiTextToText"></TextToText>
  44. <!--文生图-->
  45. <TextToImage class="contentClass" v-if="course.courseContentType === 'aiTextToImage'" ref="aiTextToImage"></TextToImage>
  46. <!--图生图-->
  47. <ImageToImage class="contentClass" v-if="course.courseContentType === 'aiImageToImage'" ref="aiImageToImage"></ImageToImage>
  48. <!--图生视频-->
  49. <ImageToVideo class="contentClass" v-if="course.courseContentType === 'aiImageToVideo'" ref="aiImageToVideo"></ImageToVideo>
  50. <!--编程地图游戏-->
  51. <MapGame v-if="course.courseContentType === 'blockly'"
  52. :game-id="course.id"
  53. :map-background="course.blocklyBackground"
  54. :map-tile-size="course.blocklyTileSize"
  55. :map-start-point="course.blocklyStartPoint"
  56. :map-end-point="course.blocklyEndPoint"
  57. :map-walkable-points="course.blocklyWalkablePoints"
  58. :user-direction="course.blocklyUserDirection"
  59. :user-image="course.blocklyUserImage"
  60. :info="course.blocklyInfo"
  61. :game-title="course.courseName"
  62. :course-list="props.courseList"
  63. :current-index="props.courseList.findIndex(item => item.id === course.id)"
  64. @close-game="emit('closeVideo')"
  65. @prev-section="playPreviousVideo"
  66. @next-section="playNextVideo"
  67. ></MapGame>
  68. </template>
  69. <!-- 视频切换按钮 - 始终显示 -->
  70. <div class="video-switch">
  71. <div class="caret-left" @click="playPreviousVideo">
  72. <el-button type="warning" round>
  73. <img :src="leftImg" alt="Left" />上一节</el-button
  74. >
  75. </div>
  76. <div class="caret-right" @click="playNextVideo">
  77. <el-button type="warning" round
  78. >下一节<img :src="rightImg" alt="Right" />
  79. </el-button>
  80. </div>
  81. </div>
  82. </div>
  83. </div>
  84. <!-- 弹框组件 -->
  85. <DialogComponents
  86. :questionDialogVisible="questionDialogVisible"
  87. :currentQuestion="courseConfig"
  88. :gradeId="gradeId"
  89. :typeId="typeId"
  90. :courseId="course.id || ''"
  91. @closeQuestionDialog="closeQuestionDialog"
  92. @submitAnswer="handleSubmitAnswer"
  93. />
  94. </div>
  95. </template>
  96. <script setup>
  97. import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
  98. import { useRouter } from 'vue-router'
  99. import { Search, ArrowLeftBold } from '@element-plus/icons-vue'
  100. import { ElMessage } from 'element-plus'
  101. import isDisabledImage from '@/assets/images/permission/isDisabled.png'
  102. import { ClassType } from '@/api/class.js'
  103. import { Message } from '@/utils/message/Message.js'
  104. import { saveRecord } from '@/api/personalized/index.js'
  105. // 导入全局状态
  106. import { globalState } from '@/utils/globalState.js'
  107. // 导入图标
  108. import leftImg from '@/assets/icon/backward.png'
  109. import rightImg from '@/assets/icon/f-backward.png'
  110. // 导入新创建的组件
  111. import VideoPlayer from '@/components/videopage/VideoPlayer.vue'
  112. import DialogComponents from '@/components/videopage/DialogComponents.vue'
  113. import PptView from "@/components/PPT/PptView.vue";
  114. import ImageView from '@/components/Image/ImageView.vue'
  115. // AI实验室
  116. import TextToText from "@/components/ai/text/TextToText.vue";
  117. import TextToImage from "@/components/ai/image/TextToImage.vue";
  118. import ImageToImage from "@/components/ai/image/ImageToImage.vue";
  119. import ImageToVideo from "@/components/ai/video/ImageToVideo.vue";
  120. import MapGame from "@/views/block/MapGame.vue";
  121. const router = useRouter() // 获取当前路由对象
  122. // 渲染页面标题
  123. const boxIconTitle = ref('')
  124. // 定义组件的props
  125. const props = defineProps({
  126. courseData: {
  127. type: Object,
  128. default: null
  129. },
  130. courseList: {
  131. type: Array,
  132. default: () => []
  133. }
  134. })
  135. // 定义emit事件
  136. const emit = defineEmits(['closeVideo'])
  137. // 课程集合数据
  138. const courseList = ref([])
  139. //当前课程 - 重新定义course来接收传递过来的数据
  140. const course = ref({})
  141. // 菜单数据
  142. const menuItems = ref([])
  143. // 课程集合数据
  144. const videoPathMap = ref({})
  145. // 已观看课程ID列表
  146. const watchedCourseIds = ref([])
  147. // 试题弹框显示状态
  148. const questionDialogVisible = ref(false)
  149. // 当前显示的试题
  150. const courseConfig = ref({})
  151. // 年级id
  152. const gradeId = ref('')
  153. // 课程大纲id
  154. const typeId = ref('')
  155. // 课程小节id
  156. const courseId = ref('')
  157. // 课程排序
  158. const typeSort = ref('')
  159. // 课程权限
  160. const courseDataScope = ref([])
  161. // 测试账号禁用视频
  162. const isDisabled = ref(false)
  163. //课程小节字典
  164. const menuDict = ref({
  165. 1: '课前回顾',
  166. 2: '课程引入',
  167. 3: '知识讲解',
  168. 4: '趣味实操',
  169. 5: '课程总结'
  170. })
  171. // 视频切换事件处理函数
  172. const switchVideo = (index) => {
  173. // 根据索引切换视频
  174. if (videoPathMap.value[index]) {
  175. course.value = videoPathMap.value[index]
  176. courseId.value = course.value.id
  177. console.log("课程id:",courseId.value)
  178. } else {
  179. //视频不存在
  180. Message().notifyWarning('视频不存在!', true)
  181. }
  182. //测试账号禁用视频
  183. if (disableVideo(index)) return
  184. }
  185. // 展平所有菜单项索引
  186. const flattenMenuItems = () => {
  187. const indices = []
  188. const traverse = items => {
  189. for (const item of items) {
  190. if (!item.children) {
  191. indices.push(item.key)
  192. } else {
  193. traverse(item.children)
  194. }
  195. }
  196. }
  197. traverse(menuItems.value)
  198. return indices
  199. }
  200. // 播放上一个视频
  201. const playPreviousVideo = () => {
  202. if (props.courseList && props.courseList.length > 0) {
  203. const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
  204. if (currentIndex > 0) {
  205. const previousCourse = props.courseList[currentIndex - 1]
  206. // 更新当前课程数据
  207. course.value = {
  208. id: previousCourse.id,
  209. courseName: previousCourse.bcName,
  210. courseContentType: previousCourse.bcContentType,
  211. courseVideoPath: previousCourse.bcContent,
  212. courseConfigList: previousCourse.blocklyConfigList,
  213. key: previousCourse.id.toString(),
  214. // blockly相关属性,用于MapGame组件
  215. blocklyBackground: previousCourse.blocklyBackground,
  216. blocklyTileSize: previousCourse.blocklyTileSize,
  217. blocklyStartPoint: previousCourse.blocklyStartPoint,
  218. blocklyEndPoint: previousCourse.blocklyEndPoint,
  219. blocklyWalkablePoints: previousCourse.blocklyWalkablePoints,
  220. blocklyUserDirection: previousCourse.blocklyUserDirection || 0,
  221. blocklyUserImage: previousCourse.blocklyUserImage,
  222. blocklyInfo: previousCourse.blocklyInfo
  223. };
  224. courseId.value = course.value.id;
  225. // 更新标题
  226. boxIconTitle.value = course.value.courseName;
  227. // 禁用视频检查
  228. disableVideo(course.value.key);
  229. }
  230. }
  231. }
  232. // 播放下一个视频
  233. const playNextVideo = () => {
  234. if (props.courseList && props.courseList.length > 0) {
  235. const currentIndex = props.courseList.findIndex(item => item.id === course.value.id)
  236. if (currentIndex !== -1 && currentIndex < props.courseList.length - 1) {
  237. const nextCourse = props.courseList[currentIndex + 1]
  238. // 更新当前课程数据
  239. course.value = {
  240. id: nextCourse.id,
  241. courseName: nextCourse.bcName,
  242. courseContentType: nextCourse.bcContentType,
  243. courseVideoPath: nextCourse.bcContent,
  244. courseConfigList: nextCourse.blocklyConfigList,
  245. key: nextCourse.id.toString(),
  246. // blockly相关属性,用于MapGame组件
  247. blocklyBackground: nextCourse.blocklyBackground,
  248. blocklyTileSize: nextCourse.blocklyTileSize,
  249. blocklyStartPoint: nextCourse.blocklyStartPoint,
  250. blocklyEndPoint: nextCourse.blocklyEndPoint,
  251. blocklyWalkablePoints: nextCourse.blocklyWalkablePoints,
  252. blocklyUserDirection: nextCourse.blocklyUserDirection || 0,
  253. blocklyUserImage: nextCourse.blocklyUserImage,
  254. blocklyInfo: nextCourse.blocklyInfo
  255. };
  256. courseId.value = course.value.id;
  257. // 更新标题
  258. boxIconTitle.value = course.value.courseName;
  259. // 禁用视频检查
  260. disableVideo(course.value.key);
  261. }
  262. }
  263. }
  264. // 播放下一个视频
  265. const handleVideoEnded = () => {
  266. // 记录当前视频ID为已观看
  267. if (
  268. course.value &&
  269. course.value.id &&
  270. !watchedCourseIds.value.includes(course.value.id)
  271. ) {
  272. watchedCourseIds.value.push(course.value.id)
  273. localStorage.setItem(
  274. 'watchedCourseIds',
  275. JSON.stringify(watchedCourseIds.value)
  276. )
  277. }
  278. // 如果是图片类型,不需要自动播放下一个
  279. if (course.value.courseContentType === 'video') {
  280. const allIndices = flattenMenuItems()
  281. const currentIndexInList = allIndices.indexOf(course.value.key)
  282. if (currentIndexInList !== -1 && currentIndexInList < allIndices.length - 1) {
  283. const nextIndex = allIndices[currentIndexInList + 1]
  284. if (videoPathMap.value[nextIndex]) {
  285. course.value = videoPathMap.value[nextIndex]
  286. courseId.value = course.value.id
  287. console.log("课程id:",courseId.value)
  288. //测试账号禁用视频
  289. if (disableVideo(nextIndex)) return
  290. } else {
  291. //视频不存在
  292. Message().notifyWarning('视频不存在!', true)
  293. }
  294. }
  295. }
  296. const allIndices = flattenMenuItems()
  297. const currentIndexInList = allIndices.indexOf(course.value.key)
  298. if (currentIndexInList !== -1 && currentIndexInList < allIndices.length - 1) {
  299. const nextIndex = allIndices[currentIndexInList + 1]
  300. if (videoPathMap.value[nextIndex]) {
  301. course.value = videoPathMap.value[nextIndex]
  302. courseId.value = course.value.id
  303. console.log("课程id:",courseId.value)
  304. //测试账号禁用视频
  305. if (disableVideo(nextIndex)) return
  306. } else {
  307. //视频不存在
  308. Message().notifyWarning('视频不存在!', true)
  309. }
  310. }
  311. }
  312. // 禁用视频
  313. const disableVideo = (index = course.value.key) => {
  314. // 未配置课程权限,不禁用视频
  315. if (!courseDataScope.value || courseDataScope.value.length === 0) {
  316. return false
  317. }
  318. //配置了课程权限,且视频id不在权限列表中
  319. isDisabled.value = !courseDataScope.value.some(item => Number(item) === videoPathMap.value[index].id)
  320. if (isDisabled.value) {
  321. Message().notifyWarning('您的账号并未开放此课程!', true)
  322. return isDisabled.value;
  323. }
  324. return isDisabled.value;
  325. }
  326. // 处理视频时间更新事件
  327. const handleVideoTimeUpdate = ({ currentTime, progressPercentage, courseConfig: config }) => {
  328. if (config) {
  329. questionDialogVisible.value = true
  330. courseConfig.value = config
  331. // 保存试题进度
  332. const saveQuestProgress = async () =>{
  333. try {
  334. // 确保courseId已经设置
  335. if (!courseId.value && typeId.value) {
  336. const courseRes = await ClassType(typeId.value)
  337. if (courseRes.data && courseRes.data.length > 0) {
  338. courseId.value = course.value && course.value.id ? course.value.id : courseRes.data[0].id
  339. }
  340. }
  341. if (config.id) {
  342. // 保存弹窗问题进度
  343. await saveRecord({
  344. brpNjId: gradeId.value,
  345. brpCtId: typeId.value,
  346. brpCourseConfigId: config.id,
  347. brpCourseId: courseId.value,
  348. brpType: 'courseQuest',
  349. brpProgress: progressPercentage
  350. })
  351. } else {
  352. console.error('无法保存试题进度: 试题id不存在')
  353. }
  354. }catch(error){
  355. console.error(`保存试题进度失败:`, error)
  356. }
  357. }
  358. // 调用异步函数
  359. saveQuestProgress()
  360. }
  361. }
  362. // 关闭试题弹框
  363. const closeQuestionDialog = () => {
  364. questionDialogVisible.value = false
  365. }
  366. // 提交答案
  367. const handleSubmitAnswer = ({ selectedOption }) => {
  368. questionDialogVisible.value = false
  369. }
  370. // 课程小节数据提取
  371. const getAllCourseSections = () => {
  372. let sections = []
  373. const traverse = items => {
  374. items.forEach(item => {
  375. if (item.children) {
  376. traverse(item.children)
  377. } else {
  378. sections.push({ title: item.title, key: item.key })
  379. }
  380. })
  381. }
  382. traverse(menuItems.value)
  383. return sections
  384. }
  385. // 初始化课程数据范围
  386. const initCourseDataScope = () => {
  387. const scope = localStorage.getItem("courseDataScope");
  388. if (scope) {
  389. courseDataScope.value = scope.split(",");
  390. }
  391. };
  392. // 处理父组件传递的课程数据
  393. const handleParentCourseData = () => {
  394. if (!props.courseData) return false;
  395. console.log('从父组件接收到的课程数据:', props.courseData);
  396. // 设置返回按钮标题
  397. boxIconTitle.value = props.courseData.bcName;
  398. // 重新定义course接收传递过来的数据
  399. course.value = {
  400. id: props.courseData.id,
  401. courseName: props.courseData.bcName,
  402. courseContentType: props.courseData.bcContentType,
  403. courseVideoPath: props.courseData.bcContent,
  404. courseConfigList: props.courseData.blocklyConfigList,
  405. key: props.courseData.id.toString(),
  406. // blockly相关属性,用于MapGame组件
  407. blocklyBackground: props.courseData.blocklyBackground,
  408. blocklyTileSize: props.courseData.blocklyTileSize,
  409. blocklyStartPoint: props.courseData.blocklyStartPoint,
  410. blocklyEndPoint: props.courseData.blocklyEndPoint,
  411. blocklyWalkablePoints: props.courseData.blocklyWalkablePoints,
  412. blocklyUserDirection: props.courseData.blocklyUserDirection || 0,
  413. blocklyUserImage: props.courseData.blocklyUserImage,
  414. blocklyInfo: props.courseData.blocklyInfo
  415. };
  416. courseId.value = course.value.id;
  417. // 如果有配置,禁用视频检查
  418. if (!disableVideo(course.value.key)) {
  419. console.log('课程已加载:', course.value);
  420. }
  421. return true;
  422. };
  423. // 处理课程数据列表
  424. const processCourseDataList = (data) => {
  425. // 对返回的课程数据进行处理,确保ccTime为有效秒数
  426. return data.map(course => {
  427. // 检查并处理courseConfigList
  428. if (course.courseConfigList && Array.isArray(course.courseConfigList)) {
  429. // 过滤掉ccTime为0的配置项
  430. const validConfigList = course.courseConfigList.filter(config =>
  431. config.ccTime !== undefined && config.ccTime !== null && config.ccTime > 0
  432. );
  433. return {
  434. ...course,
  435. courseConfigList: validConfigList
  436. };
  437. }
  438. return course;
  439. });
  440. };
  441. // 构建菜单结构
  442. const buildMenuStructure = () => {
  443. let topName = '';
  444. courseList.value.forEach((courseTemp, index) => {
  445. let menuIndex = courseTemp.courseLabel + '-' + (index + 1);
  446. // 创建菜单项
  447. let menu = {
  448. key: menuIndex,
  449. index: menuIndex,
  450. title: courseTemp.courseName
  451. };
  452. if (topName === courseTemp.courseLabel) {
  453. // 如果与上一个课程同属一个大节,添加到子菜单
  454. let topMenu = menuItems.value[menuItems.value.length - 1];
  455. let topMenuChildren = topMenu.children;
  456. if (topMenuChildren) {
  457. topMenuChildren.push(menu);
  458. } else {
  459. // 如果当前大节还没有子菜单,创建子菜单结构
  460. topMenu = {
  461. ...topMenu,
  462. title: menuDict.value[courseTemp.courseLabel],
  463. children: [
  464. {
  465. key: topMenu.key,
  466. index: topMenu.index,
  467. title: topMenu.title,
  468. },
  469. menu
  470. ]
  471. };
  472. menuItems.value[menuItems.value.length - 1] = topMenu;
  473. }
  474. } else {
  475. // 如果是新的大节,直接添加到菜单
  476. menuItems.value.push(menu);
  477. }
  478. topName = courseTemp.courseLabel;
  479. courseTemp['key'] = menuIndex;
  480. videoPathMap.value[menuIndex] = courseTemp;
  481. // 确定默认课程
  482. if (index === 0) {
  483. courseId.value = courseTemp.id;
  484. if (!disableVideo(menuIndex)) {
  485. course.value = courseTemp;
  486. } else {
  487. course.value.key = courseTemp.key;
  488. course.value.courseName = courseTemp.courseName;
  489. }
  490. }
  491. });
  492. };
  493. // 初始化已观看课程ID
  494. const initWatchedCourseIds = () => {
  495. const savedWatchedIds = localStorage.getItem('watchedCourseIds');
  496. if (savedWatchedIds) {
  497. try {
  498. watchedCourseIds.value = JSON.parse(savedWatchedIds);
  499. } catch (error) {
  500. console.error('解析已观看课程ID失败:', error);
  501. watchedCourseIds.value = [];
  502. }
  503. }
  504. };
  505. // 渲染 课程数据结构 以及 视频
  506. onMounted(async () => {
  507. // 初始化课程数据范围
  508. initCourseDataScope();
  509. // 检查是否有从父组件传递的courseData
  510. if (handleParentCourseData()) {
  511. return;
  512. }
  513. // 从路由参数获取typeId
  514. const typeIdParam = router.currentRoute.value.query.typeId;
  515. if (typeIdParam) {
  516. typeId.value = typeIdParam;
  517. try {
  518. // 取接口课程数据
  519. const res = await ClassType(typeIdParam);
  520. // 处理课程数据
  521. courseList.value = processCourseDataList(res.data);
  522. // 初始化已观看课程ID
  523. initWatchedCourseIds();
  524. // 构建菜单结构
  525. buildMenuStructure();
  526. } catch (error) {
  527. console.error('获取课程数据失败:', error);
  528. ElMessage.error('获取课程数据失败,请稍后重试');
  529. }
  530. }
  531. // 设置页面标题和排序
  532. const title = router.currentRoute.value.query.typeName;
  533. if (title) {
  534. boxIconTitle.value = String(title);
  535. }
  536. typeSort.value = router.currentRoute.value.query.typeSort;
  537. // 初始化年级ID
  538. gradeId.value = globalState.initGradeId();
  539. })
  540. onBeforeUnmount(() => {
  541. // 组件卸载时的清理
  542. })
  543. </script>
  544. <style scoped lang="scss">
  545. @use 'sass:math';
  546. @use 'sass:color'; // 引入 color 模块
  547. // 定义rpx转换函数
  548. @function rpx($px) {
  549. @return math.div($px, 750) * 100vw;
  550. }
  551. // 定义儿童风格的蓝紫色调
  552. $primary-color: rgba(106, 90, 205, 0.52); // 主色调:蓝紫色
  553. $secondary-color: rgba(147, 112, 219, 0.66); // 辅助色:亮蓝紫色
  554. $accent-color: rgb(133, 89, 220); // 强调色:暗蓝紫色
  555. $light-color: #ffffff; // 浅色背景:淡紫色
  556. $text-color: #483d8b; // 文本颜色:靛蓝色
  557. // 视频切换按钮样式
  558. .video-switch {
  559. width: 100%;
  560. display: flex;
  561. margin-top: rpx(5);
  562. margin-bottom: rpx(15);
  563. justify-content: center;
  564. }
  565. .caret-right,
  566. .caret-left {
  567. width: rpx(50);
  568. margin: 0 rpx(20);
  569. margin: auto;
  570. display: flex;
  571. justify-content: center;
  572. }
  573. .caret-left ::v-deep(.el-button.is-round),
  574. .caret-right ::v-deep(.el-button.is-round) {
  575. width: rpx(50);
  576. height: rpx(15);
  577. color: white;
  578. font-size: rpx(7);
  579. border-radius: none;
  580. border: 1px white solid;
  581. background-color: rgb(255, 255, 255, 0.5);
  582. box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
  583. }
  584. .caret-right img,
  585. .caret-left img {
  586. width: rpx(12);
  587. }
  588. .default-messages {
  589. margin-top: rpx(-10);
  590. margin-bottom: rpx(5);
  591. }
  592. /* 添加过渡样式 */
  593. .content-box {
  594. width: 100%;
  595. height: 100%;
  596. display: flex;
  597. flex-direction: column; /* 子元素上下排列 */
  598. // background: linear-gradient(to bottom, #001169, #8a78d0);
  599. background-image: url('@/assets/programming/list_bg03.png');
  600. }
  601. .home-container {
  602. position: fixed;
  603. top: 0;
  604. left: 0;
  605. right: 0;
  606. bottom: 0;
  607. // background: linear-gradient(to bottom, #001169, #b4a8e1);
  608. background-image: url('@/assets/programming/list_bg03.png');
  609. }
  610. .box-1 {
  611. width: 100%;
  612. // height: rpx(50);
  613. margin-top: rpx(10);
  614. display: flex;
  615. justify-content: center;
  616. align-items: center;
  617. box-sizing: border-box;
  618. font-size: rpx(15); // 默认字体大小
  619. }
  620. .inner-box {
  621. height: 100%;
  622. display: flex;
  623. justify-content: center;
  624. align-items: center;
  625. font-size: rpx(16); // 默认字体大小
  626. }
  627. .left-box {
  628. position: relative;
  629. justify-content: flex-start;
  630. align-items: flex-start;
  631. flex: 1; // 设置左侧盒子占比为 2
  632. cursor: pointer; // 添加鼠标指针样式
  633. }
  634. .box-icon {
  635. display: flex;
  636. align-items: center;
  637. margin-left: rpx(7);
  638. gap: 10px;
  639. padding: 10px 20px;
  640. background-color: rgba(255, 255, 255, 0.8);
  641. border-radius: 30px;
  642. backdrop-filter: blur(10px);
  643. cursor: pointer;
  644. transition: all 0.3s ease;
  645. font-size: 16px;
  646. color: #333;
  647. font-weight: 500;
  648. width: fit-content;
  649. }
  650. .box-icon:hover {
  651. background-color: rgba(255, 255, 255, 0.9);
  652. transform: translate(-3px);
  653. }
  654. .box-icon .left-icon {
  655. margin: 0;
  656. }
  657. .left-box span {
  658. position: absolute;
  659. font-size: rpx(11); // 默认字体大小
  660. color: white;
  661. }
  662. .right-box {
  663. flex: 1;
  664. position: relative; // 添加相对定位;
  665. }
  666. .top-right-box {
  667. position: absolute; // 添加绝对定位
  668. // margin-top: rpx(9); // 调整上边距离
  669. margin-left: rpx(260); // 调整右边距离
  670. width: rpx(100); // 设置盒子宽度,可按需调整
  671. // height: 60px; // 设置盒子高度,可按需调整
  672. margin-right: rpx(20);
  673. display: flex;
  674. justify-content: flex-end;
  675. }
  676. .top-right-box {
  677. ::v-deep(.el-input__wrapper) {
  678. height: rpx(15);
  679. font-size: rpx(6);
  680. background-color: rgb(255, 255, 255, 0.5);
  681. border-radius: rpx(12);
  682. border: white 1px solid;
  683. color: white;
  684. }
  685. ::v-deep(.el-input__icon) {
  686. color: white; // 设置输入框图标颜色为白色
  687. }
  688. // 添加占位符样式
  689. ::v-deep(.el-input__inner::placeholder) {
  690. color: white;
  691. }
  692. // 添加输入框文字颜色样式
  693. ::v-deep(.el-input__inner) {
  694. color: black;
  695. }
  696. }
  697. // 搜索框
  698. .search-input {
  699. width: rpx(100);
  700. height: rpx(15);
  701. font-size: rpx(7);
  702. }
  703. .box-2 {
  704. width: 100%;
  705. flex: 1;
  706. box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
  707. box-sizing: border-box;
  708. // display: flex; // 确保子元素水平排列
  709. flex-wrap: wrap; // 允许子元素换行;
  710. align-content: center; // 顶部对齐;
  711. cursor: pointer; // 添加鼠标指针样式
  712. }
  713. .small-title {
  714. width: 100%;
  715. // margin-top: rpx(-20);
  716. height: rpx(20);
  717. color: white;
  718. font-size: rpx(10);
  719. justify-content: center; //使子元素水平居中
  720. }
  721. // 图片容器样式
  722. .image-container {
  723. width: 100%;
  724. display: flex;
  725. justify-content: center;
  726. align-items: center;
  727. // padding: rpx(20) 0;
  728. }
  729. // 图片样式
  730. .course-image {
  731. max-width: 70%;
  732. max-height: rpx(400);
  733. object-fit: contain;
  734. border-radius: rpx(12);
  735. box-shadow: 0 rpx(10) rpx(20) rgba(0, 0, 0, 0.1);
  736. }
  737. // 儿童风格试题弹框样式
  738. .child-dialog {
  739. .el-dialog__header {
  740. display: none; // 隐藏原有的标题栏
  741. }
  742. .el-dialog__body {
  743. padding: rpx(20);
  744. position: relative;
  745. }
  746. .el-dialog__footer {
  747. border-top: none;
  748. padding: rpx(10) rpx(20);
  749. text-align: center;
  750. margin-top: auto; // 使底部按钮位于底部
  751. }
  752. .el-dialog__wrapper {
  753. // 修改半透明背景色
  754. background-color: rgba(0, 0, 0, 0.6);
  755. }
  756. .el-dialog {
  757. border: none;
  758. border-radius: rpx(20);
  759. background: linear-gradient(
  760. 135deg,
  761. $light-color,
  762. #d8bfd8
  763. ); // 柔和的蓝紫色渐变
  764. overflow: hidden;
  765. display: flex; // 添加 flex 布局
  766. flex-direction: column; // 设置垂直布局
  767. min-height: 0; // 防止子元素溢出
  768. // 添加装饰元素
  769. &::before {
  770. content: '';
  771. position: absolute;
  772. top: 0;
  773. left: 0;
  774. width: 100%;
  775. height: rpx(10);
  776. background: linear-gradient(90deg, $secondary-color, $accent-color);
  777. }
  778. }
  779. }
  780. // 问题标题样式
  781. .question-title {
  782. padding: rpx(15);
  783. border-radius: rpx(12);
  784. margin-bottom: rpx(20);
  785. color: #483d8b;
  786. font-weight: bold;
  787. font-size: rpx(12);
  788. position: relative;
  789. display: flex;
  790. // align-items: center;
  791. .question-icon {
  792. background-color: $accent-color;
  793. color: white;
  794. width: rpx(24);
  795. height: rpx(24);
  796. border-radius: 50%;
  797. display: flex;
  798. align-items: center;
  799. justify-content: center;
  800. margin-right: rpx(10);
  801. font-weight: bold;
  802. box-shadow: 0 rpx(2) rpx(5) rgba($accent-color, 0.3);
  803. }
  804. }
  805. // 选项容器样式
  806. .options-container {
  807. margin-bottom: rpx(20);
  808. }
  809. // 问题选项样式
  810. .question-option {
  811. margin: rpx(8) 0;
  812. padding: rpx(10) rpx(15);
  813. border-radius: rpx(12);
  814. border: rpx(1) solid rgba($primary-color, 0.3);
  815. transition: all 0.3s ease;
  816. display: flex;
  817. align-items: center;
  818. background-color: white;
  819. box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.05);
  820. ::v-deep(.el-radio__label) {
  821. color: $text-color;
  822. margin-left: rpx(8);
  823. flex: 1;
  824. text-align: left;
  825. // 增大字体大小
  826. font-size: rpx(12);
  827. }
  828. // 选中时的样式变化
  829. .el-radio__input.is-checked + .el-radio__label {
  830. font-weight: bold;
  831. color: $accent-color;
  832. }
  833. &:hover {
  834. background-color: rgba($primary-color, 0.05);
  835. border-color: rgba($primary-color, 0.5);
  836. transform: translateY(-rpx(1));
  837. }
  838. }
  839. // 暂无选项样式
  840. .no-options {
  841. color: rgba($text-color, 0.7);
  842. text-align: center;
  843. padding: rpx(20);
  844. font-size: rpx(12);
  845. }
  846. // 底部按钮样式
  847. .child-button {
  848. min-width: rpx(80);
  849. height: rpx(30);
  850. border-radius: rpx(8);
  851. font-size: rpx(12);
  852. font-weight: 500;
  853. transition: all 0.3s ease;
  854. box-shadow: 0 rpx(2) rpx(8) rgba(0, 0, 0, 0.1);
  855. &.confirm {
  856. background: linear-gradient(to bottom, #ab81ff, #8559dc);
  857. border: none;
  858. border-right: 15px;
  859. color: white;
  860. &:hover {
  861. background: linear-gradient(
  862. to bottom,
  863. color.adjust(#ab81ff, $lightness: -5%),
  864. color.adjust(#8559dc, $lightness: -5%)
  865. );
  866. transform: translateY(-rpx(1));
  867. color: white;
  868. }
  869. }
  870. &.cancel {
  871. background: white;
  872. border: rpx(1) solid rgba($primary-color, 0.3);
  873. color: $text-color;
  874. &:hover {
  875. background: rgba($primary-color, 0.05);
  876. border-color: rgba($primary-color, 0.5);
  877. transform: translateY(-rpx(1));
  878. }
  879. }
  880. }
  881. // AI对话图标样式
  882. .ai-icon-container {
  883. position: absolute;
  884. bottom: rpx(20);
  885. right: rpx(20);
  886. display: flex;
  887. flex-direction: column;
  888. align-items: center;
  889. cursor: pointer;
  890. transition: all 0.3s ease;
  891. &:hover {
  892. transform: translateY(-rpx(2));
  893. }
  894. .ai-icon {
  895. width: rpx(30);
  896. height: rpx(30);
  897. margin-bottom: rpx(0);
  898. filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
  899. // 添加过渡动画
  900. transition: transform 0.3s ease;
  901. }
  902. // 悬浮时放大效果
  903. .ai-icon:hover {
  904. transform: scale(1.5);
  905. }
  906. .ai-text {
  907. color: $text-color;
  908. font-size: rpx(8);
  909. background-color: rgba(255, 255, 255, 0.7);
  910. padding: rpx(2) rpx(5);
  911. border-radius: rpx(5);
  912. }
  913. }
  914. // AI消息样式
  915. .ai-message {
  916. display: flex;
  917. align-items: flex-start;
  918. margin-bottom: rpx(15);
  919. .ai-avatar {
  920. width: rpx(30);
  921. height: rpx(30);
  922. border-radius: 50%;
  923. margin-right: rpx(10);
  924. background-color: $primary-color;
  925. padding: rpx(5);
  926. // box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.2);
  927. }
  928. .ai-text-content {
  929. background-color: $light-color;
  930. padding: rpx(8) rpx(12);
  931. border-radius: rpx(10);
  932. font-size: rpx(10);
  933. color: $text-color;
  934. max-width: 80%;
  935. // box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  936. }
  937. }
  938. // 用户输入框样式
  939. .user-input {
  940. ::v-deep(.el-input__wrapper) {
  941. height: rpx(23);
  942. border-top-left-radius: rpx(5);
  943. border-bottom-left-radius: rpx(5);
  944. border-color: rgba($primary-color, 0.3);
  945. &:focus-within {
  946. box-shadow: 0 0 0 rpx(1) rgba($primary-color, 0.5);
  947. }
  948. }
  949. ::v-deep(.el-input__inner) {
  950. font-size: rpx(10);
  951. // color: $text-color;
  952. text-indent: 1em;
  953. }
  954. ::v-deep(.el-input-group__append, .el-input-group__prepend) {
  955. background: linear-gradient(to bottom, #ab81ff, #8559dc);
  956. border-top-right-radius: rpx(5);
  957. border-bottom-right-radius: rpx(5);
  958. color: white;
  959. font-size: rpx(9);
  960. }
  961. }
  962. /* 定义淡入和缩放动画 */
  963. .fade-scale-enter-active,
  964. .fade-scale-leave-active {
  965. transition: all 0.5s ease;
  966. }
  967. .fade-scale-enter-from,
  968. .fade-scale-leave-to {
  969. opacity: 0.1;
  970. transform: scale(0.9);
  971. }
  972. // 自定义试题弹框背景
  973. .child-dialog-wrapper {
  974. position: fixed;
  975. top: 0;
  976. left: 0;
  977. right: 0;
  978. bottom: 0;
  979. background-color: rgba(0, 0, 0, 0.6); // 半透明背景
  980. display: flex;
  981. justify-content: center;
  982. align-items: center;
  983. z-index: 1000;
  984. }
  985. .child-dialog {
  986. border: none;
  987. border-radius: rpx(15);
  988. background: rgb(255, 255, 255, 0.8); // 柔和的蓝紫色渐变
  989. overflow: hidden;
  990. padding: rpx(5);
  991. // width: 40%;
  992. // height: 60%;
  993. position: relative;
  994. }
  995. // AI对话弹框样式
  996. .ai-dialog-wrapper {
  997. position: fixed;
  998. top: 0;
  999. left: 0;
  1000. right: 0;
  1001. bottom: 0;
  1002. display: flex;
  1003. justify-content: flex-end;
  1004. align-items: center;
  1005. z-index: 1001;
  1006. pointer-events: none;
  1007. }
  1008. .ai-dialog {
  1009. border: none;
  1010. border-radius: rpx(15);
  1011. background: rgb(255, 255, 255, 0.8);
  1012. overflow: hidden;
  1013. padding: rpx(20);
  1014. width: 30%;
  1015. // 增加高度
  1016. height: 80%;
  1017. margin-right: rpx(50);
  1018. pointer-events: auto;
  1019. position: relative;
  1020. display: flex;
  1021. flex-direction: column;
  1022. &::before {
  1023. content: '';
  1024. position: absolute;
  1025. top: 0;
  1026. left: 0;
  1027. width: 100%;
  1028. height: rpx(10);
  1029. }
  1030. }
  1031. .ai-dialog-header {
  1032. position: relative;
  1033. display: flex;
  1034. justify-content: center;
  1035. align-items: center;
  1036. margin-bottom: rpx(15);
  1037. margin-top: rpx(-10);
  1038. img {
  1039. width: rpx(15);
  1040. }
  1041. h3 {
  1042. color: black;
  1043. font-size: rpx(12);
  1044. }
  1045. .close-btn {
  1046. padding: 0;
  1047. width: rpx(20);
  1048. height: rpx(20);
  1049. font-size: rpx(16);
  1050. line-height: 1;
  1051. position: absolute;
  1052. background-color: transparent;
  1053. border: none;
  1054. margin-left: rpx(200);
  1055. }
  1056. }
  1057. .ai-dialog-content {
  1058. flex: 1;
  1059. display: flex;
  1060. flex-direction: column;
  1061. }
  1062. .ai-message-history {
  1063. flex: 1;
  1064. // 当内容超出容器高度时,显示垂直滚动条
  1065. overflow-y: auto;
  1066. margin-bottom: rpx(15);
  1067. // 可以根据实际情况调整最大高度
  1068. font-size: rpx(5);
  1069. max-height: 50vh;
  1070. // padding: 5px 10px;
  1071. .message {
  1072. display: flex;
  1073. align-items: flex-start;
  1074. margin-bottom: rpx(10);
  1075. &.user {
  1076. flex-direction: row-reverse;
  1077. }
  1078. .avatar {
  1079. width: rpx(30);
  1080. height: rpx(30);
  1081. border-radius: 50%;
  1082. margin: 0 rpx(10);
  1083. }
  1084. .user {
  1085. width: 15px;
  1086. height: 15px;
  1087. }
  1088. .message-content {
  1089. background-color: #ffffff;
  1090. font-size: rpx(5);
  1091. max-width: 80%;
  1092. color: black;
  1093. box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  1094. }
  1095. }
  1096. // 滚动条整体样式
  1097. &::-webkit-scrollbar {
  1098. width: rpx(4); // 滚动条宽度
  1099. }
  1100. // 滚动条滑块样式
  1101. &::-webkit-scrollbar-thumb {
  1102. background-color: $primary-color; // 滑块颜色
  1103. border-radius: rpx(4); // 滑块圆角
  1104. transition: background-color 0.3s ease; // 颜色过渡效果
  1105. &:hover {
  1106. //background-color: darken($primary-color, 10%); // 悬停时滑块颜色加深
  1107. }
  1108. }
  1109. // 滚动条轨道样式
  1110. &::-webkit-scrollbar-track {
  1111. background-color: rgba($primary-color, 0.2); // 轨道颜色
  1112. border-radius: rpx(4); // 轨道圆角
  1113. }
  1114. .message {
  1115. display: flex;
  1116. align-items: flex-start;
  1117. margin-bottom: rpx(10);
  1118. &.user {
  1119. flex-direction: row-reverse;
  1120. }
  1121. .avatar {
  1122. width: rpx(30);
  1123. height: rpx(30);
  1124. border-radius: 50%;
  1125. margin: 0 rpx(10);
  1126. }
  1127. .message-content {
  1128. background-color: white;
  1129. padding: rpx(8) rpx(12);
  1130. border-radius: rpx(5);
  1131. font-size: rpx(8);
  1132. color: black;
  1133. max-width: 80%;
  1134. box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  1135. }
  1136. }
  1137. }
  1138. // 优化发送按钮样式
  1139. .send-button {
  1140. //background: linear-gradient(90deg, $primary-color, $secondary-color);
  1141. border: none;
  1142. color: white;
  1143. &:hover {
  1144. //background: linear-gradient(90deg, darken($primary-color, 5%), darken($secondary-color, 5%));
  1145. }
  1146. }
  1147. </style>
  1148. <style lang="scss">
  1149. // 搜索下拉框样式
  1150. @use 'sass:math';
  1151. // 定义rpx转换函数
  1152. @function rpx($px) {
  1153. @return math.div($px, 750) * 100vw;
  1154. }
  1155. /* 消除小三角 */
  1156. .el-popper__arrow {
  1157. display: none;
  1158. }
  1159. .el-popper.is-light,
  1160. .el-dropdown__popper.el-popper {
  1161. background: transparent;
  1162. border: none;
  1163. box-shadow: none;
  1164. }
  1165. .el-dropdown__popper {
  1166. --el-dropdown-menuItem-hover-color: none;
  1167. }
  1168. .el-autocomplete-suggestion .el-scrollbar__wrap {
  1169. margin: 0 auto;
  1170. background-color: rgba(255, 255, 255, 0.7);
  1171. border: 2px solid white;
  1172. border-radius: rpx(5);
  1173. backdrop-filter: blur(rpx(5));
  1174. }
  1175. .el-autocomplete-suggestion li {
  1176. color: black;
  1177. font-size: rpx(7);
  1178. padding: rpx(5) rpx(8); // 调整下拉项内边距
  1179. }
  1180. .el-autocomplete-suggestion li:hover {
  1181. background: linear-gradient(to bottom, #ffefb0, #ffcc00);
  1182. }
  1183. </style>
  1184. <style scoped lang="scss">
  1185. @use 'sass:math';
  1186. // 定义rpx转换函数
  1187. @function rpx($px) {
  1188. @return math.div($px, 750) * 100vw;
  1189. }
  1190. .default-messages {
  1191. margin-top: rpx(-10);
  1192. margin-bottom: rpx(5);
  1193. }
  1194. .contentClass{
  1195. width: 70%;
  1196. height: 80%;
  1197. margin: 0 auto;
  1198. border-radius: rpx(15);
  1199. overflow: hidden;
  1200. }
  1201. </style>