Interface.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345
  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 :disabled="props.courseList.findIndex(item => item.id === course.id) === 0">
  73. <img :src="leftImg" alt="Left" />上一节</el-button
  74. >
  75. </div>
  76. <div class="caret-right" @click="playNextVideo">
  77. <el-button type="warning" round :disabled="props.courseList.findIndex(item => item.id === course.id) === props.courseList.length - 1"
  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 "@/components/blockly/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-image: url('@/assets/programming/list_bg03.png');
  599. overflow-y: auto;
  600. // 滚动条整体样式
  601. &::-webkit-scrollbar {
  602. width: rpx(1); // 滚动条宽度
  603. }
  604. // 滚动条滑块样式
  605. &::-webkit-scrollbar-thumb {
  606. background-color: rgba(255, 255, 255, 0.5); // 滑块颜色
  607. border-radius: 4px; // 滑块圆角
  608. transition: background-color 0.3s ease;
  609. }
  610. // 滚动条轨道样式
  611. &::-webkit-scrollbar-track {
  612. background-color: rgba(0, 0, 0, 0.1); // 轨道颜色
  613. border-radius: 4px; // 轨道圆角
  614. }
  615. }
  616. .home-container {
  617. position: fixed;
  618. top: 0;
  619. left: 0;
  620. right: 0;
  621. bottom: 0;
  622. // background: linear-gradient(to bottom, #001169, #b4a8e1);
  623. background-image: url('@/assets/programming/list_bg03.png');
  624. }
  625. .box-1 {
  626. width: 100%;
  627. // height: rpx(50);
  628. margin-top: rpx(10);
  629. display: flex;
  630. justify-content: center;
  631. align-items: center;
  632. box-sizing: border-box;
  633. font-size: rpx(15); // 默认字体大小
  634. }
  635. .inner-box {
  636. height: 100%;
  637. display: flex;
  638. justify-content: center;
  639. align-items: center;
  640. font-size: rpx(16); // 默认字体大小
  641. }
  642. .left-box {
  643. position: relative;
  644. justify-content: flex-start;
  645. align-items: flex-start;
  646. flex: 1; // 设置左侧盒子占比为 2
  647. cursor: pointer; // 添加鼠标指针样式
  648. }
  649. .box-icon {
  650. display: flex;
  651. align-items: center;
  652. margin-left: rpx(7);
  653. gap: 10px;
  654. padding: 10px 20px;
  655. background-color: rgba(255, 255, 255, 0.8);
  656. border-radius: 30px;
  657. backdrop-filter: blur(10px);
  658. cursor: pointer;
  659. transition: all 0.3s ease;
  660. font-size: 16px;
  661. color: #333;
  662. font-weight: 500;
  663. width: fit-content;
  664. }
  665. .box-icon:hover {
  666. background-color: rgba(255, 255, 255, 0.9);
  667. transform: translate(-3px);
  668. }
  669. .box-icon .left-icon {
  670. margin: 0;
  671. }
  672. .left-box span {
  673. position: absolute;
  674. font-size: rpx(11); // 默认字体大小
  675. color: white;
  676. }
  677. .right-box {
  678. flex: 1;
  679. position: relative; // 添加相对定位;
  680. }
  681. .top-right-box {
  682. position: absolute; // 添加绝对定位
  683. // margin-top: rpx(9); // 调整上边距离
  684. margin-left: rpx(260); // 调整右边距离
  685. width: rpx(100); // 设置盒子宽度,可按需调整
  686. // height: 60px; // 设置盒子高度,可按需调整
  687. margin-right: rpx(20);
  688. display: flex;
  689. justify-content: flex-end;
  690. }
  691. .top-right-box {
  692. ::v-deep(.el-input__wrapper) {
  693. height: rpx(15);
  694. font-size: rpx(6);
  695. background-color: rgb(255, 255, 255, 0.5);
  696. border-radius: rpx(12);
  697. border: white 1px solid;
  698. color: white;
  699. }
  700. ::v-deep(.el-input__icon) {
  701. color: white; // 设置输入框图标颜色为白色
  702. }
  703. // 添加占位符样式
  704. ::v-deep(.el-input__inner::placeholder) {
  705. color: white;
  706. }
  707. // 添加输入框文字颜色样式
  708. ::v-deep(.el-input__inner) {
  709. color: black;
  710. }
  711. }
  712. // 搜索框
  713. .search-input {
  714. width: rpx(100);
  715. height: rpx(15);
  716. font-size: rpx(7);
  717. }
  718. .box-2 {
  719. width: 100%;
  720. flex: 1;
  721. box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
  722. box-sizing: border-box;
  723. // display: flex; // 确保子元素水平排列
  724. flex-wrap: wrap; // 允许子元素换行;
  725. align-content: center; // 顶部对齐;
  726. cursor: pointer; // 添加鼠标指针样式
  727. }
  728. .small-title {
  729. width: 100%;
  730. // margin-top: rpx(-20);
  731. height: rpx(20);
  732. color: white;
  733. font-size: rpx(10);
  734. justify-content: center; //使子元素水平居中
  735. }
  736. // 图片容器样式
  737. .image-container {
  738. width: 100%;
  739. display: flex;
  740. justify-content: center;
  741. align-items: center;
  742. // padding: rpx(20) 0;
  743. }
  744. // 图片样式
  745. .course-image {
  746. max-width: 70%;
  747. max-height: rpx(400);
  748. object-fit: contain;
  749. border-radius: rpx(12);
  750. box-shadow: 0 rpx(10) rpx(20) rgba(0, 0, 0, 0.1);
  751. }
  752. // 儿童风格试题弹框样式
  753. .child-dialog {
  754. .el-dialog__header {
  755. display: none; // 隐藏原有的标题栏
  756. }
  757. .el-dialog__body {
  758. padding: rpx(20);
  759. position: relative;
  760. }
  761. .el-dialog__footer {
  762. border-top: none;
  763. padding: rpx(10) rpx(20);
  764. text-align: center;
  765. margin-top: auto; // 使底部按钮位于底部
  766. }
  767. .el-dialog__wrapper {
  768. // 修改半透明背景色
  769. background-color: rgba(0, 0, 0, 0.6);
  770. }
  771. .el-dialog {
  772. border: none;
  773. border-radius: rpx(20);
  774. background: linear-gradient(
  775. 135deg,
  776. $light-color,
  777. #d8bfd8
  778. ); // 柔和的蓝紫色渐变
  779. overflow: hidden;
  780. display: flex; // 添加 flex 布局
  781. flex-direction: column; // 设置垂直布局
  782. min-height: 0; // 防止子元素溢出
  783. // 添加装饰元素
  784. &::before {
  785. content: '';
  786. position: absolute;
  787. top: 0;
  788. left: 0;
  789. width: 100%;
  790. height: rpx(10);
  791. background: linear-gradient(90deg, $secondary-color, $accent-color);
  792. }
  793. }
  794. }
  795. // 问题标题样式
  796. .question-title {
  797. padding: rpx(15);
  798. border-radius: rpx(12);
  799. margin-bottom: rpx(20);
  800. color: #483d8b;
  801. font-weight: bold;
  802. font-size: rpx(12);
  803. position: relative;
  804. display: flex;
  805. // align-items: center;
  806. .question-icon {
  807. background-color: $accent-color;
  808. color: white;
  809. width: rpx(24);
  810. height: rpx(24);
  811. border-radius: 50%;
  812. display: flex;
  813. align-items: center;
  814. justify-content: center;
  815. margin-right: rpx(10);
  816. font-weight: bold;
  817. box-shadow: 0 rpx(2) rpx(5) rgba($accent-color, 0.3);
  818. }
  819. }
  820. // 选项容器样式
  821. .options-container {
  822. margin-bottom: rpx(20);
  823. }
  824. // 问题选项样式
  825. .question-option {
  826. margin: rpx(8) 0;
  827. padding: rpx(10) rpx(15);
  828. border-radius: rpx(12);
  829. border: rpx(1) solid rgba($primary-color, 0.3);
  830. transition: all 0.3s ease;
  831. display: flex;
  832. align-items: center;
  833. background-color: white;
  834. box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.05);
  835. ::v-deep(.el-radio__label) {
  836. color: $text-color;
  837. margin-left: rpx(8);
  838. flex: 1;
  839. text-align: left;
  840. // 增大字体大小
  841. font-size: rpx(12);
  842. }
  843. // 选中时的样式变化
  844. .el-radio__input.is-checked + .el-radio__label {
  845. font-weight: bold;
  846. color: $accent-color;
  847. }
  848. &:hover {
  849. background-color: rgba($primary-color, 0.05);
  850. border-color: rgba($primary-color, 0.5);
  851. transform: translateY(-rpx(1));
  852. }
  853. }
  854. // 暂无选项样式
  855. .no-options {
  856. color: rgba($text-color, 0.7);
  857. text-align: center;
  858. padding: rpx(20);
  859. font-size: rpx(12);
  860. }
  861. // 底部按钮样式
  862. .child-button {
  863. min-width: rpx(80);
  864. height: rpx(30);
  865. border-radius: rpx(8);
  866. font-size: rpx(12);
  867. font-weight: 500;
  868. transition: all 0.3s ease;
  869. box-shadow: 0 rpx(2) rpx(8) rgba(0, 0, 0, 0.1);
  870. &.confirm {
  871. background: linear-gradient(to bottom, #ab81ff, #8559dc);
  872. border: none;
  873. border-right: 15px;
  874. color: white;
  875. &:hover {
  876. background: linear-gradient(
  877. to bottom,
  878. color.adjust(#ab81ff, $lightness: -5%),
  879. color.adjust(#8559dc, $lightness: -5%)
  880. );
  881. transform: translateY(-rpx(1));
  882. color: white;
  883. }
  884. }
  885. &.cancel {
  886. background: white;
  887. border: rpx(1) solid rgba($primary-color, 0.3);
  888. color: $text-color;
  889. &:hover {
  890. background: rgba($primary-color, 0.05);
  891. border-color: rgba($primary-color, 0.5);
  892. transform: translateY(-rpx(1));
  893. }
  894. }
  895. }
  896. // AI对话图标样式
  897. .ai-icon-container {
  898. position: absolute;
  899. bottom: rpx(20);
  900. right: rpx(20);
  901. display: flex;
  902. flex-direction: column;
  903. align-items: center;
  904. cursor: pointer;
  905. transition: all 0.3s ease;
  906. &:hover {
  907. transform: translateY(-rpx(2));
  908. }
  909. .ai-icon {
  910. width: rpx(30);
  911. height: rpx(30);
  912. margin-bottom: rpx(0);
  913. filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
  914. // 添加过渡动画
  915. transition: transform 0.3s ease;
  916. }
  917. // 悬浮时放大效果
  918. .ai-icon:hover {
  919. transform: scale(1.5);
  920. }
  921. .ai-text {
  922. color: $text-color;
  923. font-size: rpx(8);
  924. background-color: rgba(255, 255, 255, 0.7);
  925. padding: rpx(2) rpx(5);
  926. border-radius: rpx(5);
  927. }
  928. }
  929. // AI消息样式
  930. .ai-message {
  931. display: flex;
  932. align-items: flex-start;
  933. margin-bottom: rpx(15);
  934. .ai-avatar {
  935. width: rpx(30);
  936. height: rpx(30);
  937. border-radius: 50%;
  938. margin-right: rpx(10);
  939. background-color: $primary-color;
  940. padding: rpx(5);
  941. // box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.2);
  942. }
  943. .ai-text-content {
  944. background-color: $light-color;
  945. padding: rpx(8) rpx(12);
  946. border-radius: rpx(10);
  947. font-size: rpx(10);
  948. color: $text-color;
  949. max-width: 80%;
  950. // box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  951. }
  952. }
  953. // 用户输入框样式
  954. .user-input {
  955. ::v-deep(.el-input__wrapper) {
  956. height: rpx(23);
  957. border-top-left-radius: rpx(5);
  958. border-bottom-left-radius: rpx(5);
  959. border-color: rgba($primary-color, 0.3);
  960. &:focus-within {
  961. box-shadow: 0 0 0 rpx(1) rgba($primary-color, 0.5);
  962. }
  963. }
  964. ::v-deep(.el-input__inner) {
  965. font-size: rpx(10);
  966. // color: $text-color;
  967. text-indent: 1em;
  968. }
  969. ::v-deep(.el-input-group__append, .el-input-group__prepend) {
  970. background: linear-gradient(to bottom, #ab81ff, #8559dc);
  971. border-top-right-radius: rpx(5);
  972. border-bottom-right-radius: rpx(5);
  973. color: white;
  974. font-size: rpx(9);
  975. }
  976. }
  977. /* 定义淡入和缩放动画 */
  978. .fade-scale-enter-active,
  979. .fade-scale-leave-active {
  980. transition: all 0.5s ease;
  981. }
  982. .fade-scale-enter-from,
  983. .fade-scale-leave-to {
  984. opacity: 0.1;
  985. transform: scale(0.9);
  986. }
  987. // 自定义试题弹框背景
  988. .child-dialog-wrapper {
  989. position: fixed;
  990. top: 0;
  991. left: 0;
  992. right: 0;
  993. bottom: 0;
  994. background-color: rgba(0, 0, 0, 0.6); // 半透明背景
  995. display: flex;
  996. justify-content: center;
  997. align-items: center;
  998. z-index: 1000;
  999. }
  1000. .child-dialog {
  1001. border: none;
  1002. border-radius: rpx(15);
  1003. background: rgb(255, 255, 255, 0.8); // 柔和的蓝紫色渐变
  1004. overflow: hidden;
  1005. padding: rpx(5);
  1006. // width: 40%;
  1007. // height: 60%;
  1008. position: relative;
  1009. }
  1010. // AI对话弹框样式
  1011. .ai-dialog-wrapper {
  1012. position: fixed;
  1013. top: 0;
  1014. left: 0;
  1015. right: 0;
  1016. bottom: 0;
  1017. display: flex;
  1018. justify-content: flex-end;
  1019. align-items: center;
  1020. z-index: 1001;
  1021. pointer-events: none;
  1022. }
  1023. .ai-dialog {
  1024. border: none;
  1025. border-radius: rpx(15);
  1026. background: rgb(255, 255, 255, 0.8);
  1027. overflow: hidden;
  1028. padding: rpx(20);
  1029. width: 30%;
  1030. // 增加高度
  1031. height: 80%;
  1032. margin-right: rpx(50);
  1033. pointer-events: auto;
  1034. position: relative;
  1035. display: flex;
  1036. flex-direction: column;
  1037. &::before {
  1038. content: '';
  1039. position: absolute;
  1040. top: 0;
  1041. left: 0;
  1042. width: 100%;
  1043. height: rpx(10);
  1044. }
  1045. }
  1046. .ai-dialog-header {
  1047. position: relative;
  1048. display: flex;
  1049. justify-content: center;
  1050. align-items: center;
  1051. margin-bottom: rpx(15);
  1052. margin-top: rpx(-10);
  1053. img {
  1054. width: rpx(15);
  1055. }
  1056. h3 {
  1057. color: black;
  1058. font-size: rpx(12);
  1059. }
  1060. .close-btn {
  1061. padding: 0;
  1062. width: rpx(20);
  1063. height: rpx(20);
  1064. font-size: rpx(16);
  1065. line-height: 1;
  1066. position: absolute;
  1067. background-color: transparent;
  1068. border: none;
  1069. margin-left: rpx(200);
  1070. }
  1071. }
  1072. .ai-dialog-content {
  1073. flex: 1;
  1074. display: flex;
  1075. flex-direction: column;
  1076. }
  1077. .ai-message-history {
  1078. flex: 1;
  1079. // 当内容超出容器高度时,显示垂直滚动条
  1080. overflow-y: auto;
  1081. margin-bottom: rpx(15);
  1082. // 可以根据实际情况调整最大高度
  1083. font-size: rpx(5);
  1084. max-height: 50vh;
  1085. // padding: 5px 10px;
  1086. .message {
  1087. display: flex;
  1088. align-items: flex-start;
  1089. margin-bottom: rpx(10);
  1090. &.user {
  1091. flex-direction: row-reverse;
  1092. }
  1093. .avatar {
  1094. width: rpx(30);
  1095. height: rpx(30);
  1096. border-radius: 50%;
  1097. margin: 0 rpx(10);
  1098. }
  1099. .user {
  1100. width: 15px;
  1101. height: 15px;
  1102. }
  1103. .message-content {
  1104. background-color: #ffffff;
  1105. font-size: rpx(5);
  1106. max-width: 80%;
  1107. color: black;
  1108. box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  1109. }
  1110. }
  1111. // 滚动条整体样式
  1112. &::-webkit-scrollbar {
  1113. width: rpx(4); // 滚动条宽度
  1114. }
  1115. // 滚动条滑块样式
  1116. &::-webkit-scrollbar-thumb {
  1117. background-color: $primary-color; // 滑块颜色
  1118. border-radius: rpx(4); // 滑块圆角
  1119. transition: background-color 0.3s ease; // 颜色过渡效果
  1120. &:hover {
  1121. //background-color: darken($primary-color, 10%); // 悬停时滑块颜色加深
  1122. }
  1123. }
  1124. // 滚动条轨道样式
  1125. &::-webkit-scrollbar-track {
  1126. background-color: rgba($primary-color, 0.2); // 轨道颜色
  1127. border-radius: rpx(4); // 轨道圆角
  1128. }
  1129. .message {
  1130. display: flex;
  1131. align-items: flex-start;
  1132. margin-bottom: rpx(10);
  1133. &.user {
  1134. flex-direction: row-reverse;
  1135. }
  1136. .avatar {
  1137. width: rpx(30);
  1138. height: rpx(30);
  1139. border-radius: 50%;
  1140. margin: 0 rpx(10);
  1141. }
  1142. .message-content {
  1143. background-color: white;
  1144. padding: rpx(8) rpx(12);
  1145. border-radius: rpx(5);
  1146. font-size: rpx(8);
  1147. color: black;
  1148. max-width: 80%;
  1149. box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
  1150. }
  1151. }
  1152. }
  1153. // 优化发送按钮样式
  1154. .send-button {
  1155. //background: linear-gradient(90deg, $primary-color, $secondary-color);
  1156. border: none;
  1157. color: white;
  1158. &:hover {
  1159. //background: linear-gradient(90deg, darken($primary-color, 5%), darken($secondary-color, 5%));
  1160. }
  1161. }
  1162. </style>
  1163. <style lang="scss">
  1164. // 搜索下拉框样式
  1165. @use 'sass:math';
  1166. // 定义rpx转换函数
  1167. @function rpx($px) {
  1168. @return math.div($px, 750) * 100vw;
  1169. }
  1170. /* 消除小三角 */
  1171. .el-popper__arrow {
  1172. display: none;
  1173. }
  1174. .el-popper.is-light,
  1175. .el-dropdown__popper.el-popper {
  1176. background: transparent;
  1177. border: none;
  1178. box-shadow: none;
  1179. }
  1180. .el-dropdown__popper {
  1181. --el-dropdown-menuItem-hover-color: none;
  1182. }
  1183. .el-autocomplete-suggestion .el-scrollbar__wrap {
  1184. margin: 0 auto;
  1185. background-color: rgba(255, 255, 255, 0.7);
  1186. border: 2px solid white;
  1187. border-radius: rpx(5);
  1188. backdrop-filter: blur(rpx(5));
  1189. }
  1190. .el-autocomplete-suggestion li {
  1191. color: black;
  1192. font-size: rpx(7);
  1193. padding: rpx(5) rpx(8); // 调整下拉项内边距
  1194. }
  1195. .el-autocomplete-suggestion li:hover {
  1196. background: linear-gradient(to bottom, #ffefb0, #ffcc00);
  1197. }
  1198. </style>
  1199. <style scoped lang="scss">
  1200. @use 'sass:math';
  1201. // 定义rpx转换函数
  1202. @function rpx($px) {
  1203. @return math.div($px, 750) * 100vw;
  1204. }
  1205. .default-messages {
  1206. margin-top: rpx(-10);
  1207. margin-bottom: rpx(5);
  1208. }
  1209. .contentClass{
  1210. width: 70%;
  1211. height: 80%;
  1212. margin: 0 auto;
  1213. border-radius: rpx(15);
  1214. overflow: hidden;
  1215. }
  1216. </style>