ProgrammingList.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <!-- 编程课列表 -->
  2. <template>
  3. <div class="programming-content">
  4. <!-- 标题部分 -->
  5. <div class="top-box">
  6. <div class="top-left-box">
  7. <div class="top-left-inner-box">
  8. <!-- 左侧返回图标 -->
  9. <el-icon class="left-icon" @click="goBackIndex"><ArrowLeftBold /></el-icon>
  10. <span class="left-text">AI编程课</span>
  11. </div>
  12. </div>
  13. <div class="top-center-box">
  14. <div class="top-center-inner-box" style="background-image: url('./src/assets/programming/list_title.png');">
  15. <span>{{ pageTitle }}</span>
  16. </div>
  17. </div>
  18. <div class="top-right-box">
  19. <div class="top-right-inner-box"></div>
  20. </div>
  21. </div>
  22. <!-- 编程部分 -->
  23. <div class="middle-wrapper">
  24. <!-- 左切换按钮 -->
  25. <div class="carousel-btn prev-btn" @click="prevSlide" :class="{ 'disabled-btn': currentIndex === 0 }" :disabled="currentIndex === 0">
  26. <el-icon class="btn-icon" :class="{ 'disabled-icon': currentIndex === 0 }"><ArrowLeftBold /></el-icon>
  27. </div>
  28. <div
  29. class="middle-box"
  30. ref="middleBox"
  31. @mousedown="handleMouseDown"
  32. @mousemove="handleMouseMove"
  33. @mouseup="handleMouseUp"
  34. @mouseleave="handleMouseLeave"
  35. >
  36. <div
  37. v-for="(courseType, index) in specificCourses"
  38. :key="index"
  39. class="middle-inner-box"
  40. @click="goToProgrammingList(courseType, index)"
  41. >
  42. <div class="new-white-box"
  43. :class="{ 'active': activeButton === index, 'disabled': courseType.isDisabled }"
  44. :style="{ '--lock-image': `url(${lockImage})` }"
  45. @click="goToProgrammingList(courseType, index)">
  46. <!-- 列表封面图 -->
  47. <div class="bg-image-container" :style="{ backgroundImage: `url(${courseType.bgImage})` }">
  48. <!-- 星星图标 -->
  49. <div
  50. v-if="!courseType.isDisabled"
  51. v-for="starIndex in 5"
  52. :key="starIndex"
  53. class="star-icon"
  54. :style="{
  55. backgroundImage: `url(${starIndex <= courseType.progress ? star01Image : star02Image})`,
  56. left: `${30 + (starIndex - 1) * 30}px`
  57. }"
  58. ></div>
  59. </div>
  60. <div class="text-container">
  61. <div class="box-title">
  62. <span>{{ courseType.title }}</span>
  63. </div>
  64. </div>
  65. <!-- unlock背景图盒子 -->
  66. <div class="unlock-box" :style="{ backgroundImage: courseType.isDisabled ? `url(${lockImage})` : courseType.progress > 0 ? `url(${trophyImage})` : `url(${unlockImage})` }"></div>
  67. </div>
  68. </div>
  69. </div>
  70. <!-- 右切换按钮 -->
  71. <div class="carousel-btn next-btn" @click="nextSlide" :class="{ 'disabled-btn': currentIndex >= specificCourses.length - 3 }" :disabled="currentIndex >= specificCourses.length - 3">
  72. <el-icon class="btn-icon" :class="{ 'disabled-icon': currentIndex >= specificCourses.length - 3 }"><ArrowRightBold /></el-icon>
  73. </div>
  74. </div>
  75. <!-- 底部切换按钮 -->
  76. <div class="bottom-box">
  77. <div class="line-container">
  78. <div class="bold-line"></div>
  79. <div
  80. v-for="(button, index) in circleButtons"
  81. :key="index"
  82. class="circle-button"
  83. :class="{ 'active': activeButton === index }"
  84. @click="activeButton = index"
  85. >
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </template>
  91. <script setup>
  92. import { ref, reactive, watch, onMounted, onUnmounted } from 'vue'
  93. // 返回图标
  94. import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
  95. // 导入路由
  96. import { useRouter, useRoute } from 'vue-router';
  97. // 获取类型列表
  98. import { getTypeByThemeId } from '@/api/programming/index.js'
  99. // 导入图片
  100. import lockImage from '@/assets/programming/lock.png'
  101. import unlockImage from '@/assets/programming/unlock.png'
  102. import trophyImage from '@/assets/programming/trophy.png'
  103. // 星星图片
  104. import star02Image from '@/assets/programming/star02.png'
  105. import star01Image from '@/assets/programming/star01.png'
  106. import {Message} from "@/utils/message/Message.js";
  107. // 获取路由实例
  108. const router = useRouter()
  109. // 获取当前路由信息
  110. const route = useRoute()
  111. // 页面标题
  112. const pageTitle = ref('')
  113. // 课程类别ID
  114. const categoryId = ref('')
  115. // 课程权限
  116. const blocklyDataScope = ref([])
  117. // 测试账号禁用视频
  118. const isDisabled = ref(false)
  119. // 返回上一页
  120. const goBackIndex = () => {
  121. router.push('/programming')
  122. }
  123. // 定义课程数据
  124. const specificCourses = reactive([])
  125. // 当前激活的按钮索引
  126. const activeButton = ref(0)
  127. // 轮播图当前索引
  128. const currentIndex = ref(0)
  129. // 在获取到categoryId后再调用获取类型列表getTypeByThemeId接口
  130. const fetchTopicList = () => {
  131. if (categoryId.value) {
  132. getTypeByThemeId(categoryId.value).then(res => {
  133. console.log(categoryId.value, res);
  134. // 更新课程数据,使用接口返回的数据
  135. if (res && res.data && Array.isArray(res.data)) {
  136. // 清空原有数据
  137. specificCourses.splice(0, specificCourses.length);
  138. res.data.forEach(item => {
  139. specificCourses.push({
  140. id: item.id,
  141. title: item.ctType,
  142. bgImage: item.ctTypeImage,
  143. progress: item.progress,
  144. isDisabled: disableBlockly(item.id)
  145. });
  146. });
  147. // 更新圆形按钮数据
  148. circleButtons.splice(0, circleButtons.length);
  149. specificCourses.forEach((_, index) => {
  150. circleButtons.push({ text: String(index + 1) });
  151. });
  152. // 重置激活按钮索引,默认选中第一项
  153. activeButton.value = 0;
  154. }
  155. })
  156. }
  157. }
  158. // 定义圆形按钮数据(根据课程数量动态生成)
  159. const circleButtons = reactive(specificCourses.map((_, index) => ({ text: String(index + 1) })))
  160. // 自动滚动到中间位置
  161. watch(activeButton, (newIndex) => {
  162. if (middleBox.value && newIndex !== -1) {
  163. // 找到对应的课程卡片元素
  164. const courseElement = middleBox.value.querySelector(`.middle-inner-box:nth-child(${newIndex + 1})`);
  165. if (courseElement) {
  166. // 计算滚动位置,选中的卡片居中
  167. const containerWidth = middleBox.value.clientWidth;
  168. const elementLeft = courseElement.offsetLeft;
  169. const elementWidth = courseElement.offsetWidth;
  170. // 计算居中位置
  171. const scrollPosition = elementLeft - (containerWidth / 2) + (elementWidth / 2);
  172. // 平滑滚动到计算的位置
  173. middleBox.value.scrollTo({
  174. left: scrollPosition,
  175. behavior: 'smooth'
  176. });
  177. }
  178. }
  179. });
  180. // 拖动相关变量
  181. const isDragging = ref(false)
  182. const startX = ref(0)
  183. const scrollLeft = ref(0)
  184. // 鼠标按下事件处理函数
  185. const handleMouseDown = (e) => {
  186. isDragging.value = true
  187. startX.value = e.pageX - middleBox.value.offsetLeft
  188. scrollLeft.value = middleBox.value.scrollLeft
  189. }
  190. // 鼠标移动事件处理函数
  191. const handleMouseMove = (e) => {
  192. if (!isDragging.value) return
  193. e.preventDefault()
  194. const x = e.pageX - middleBox.value.offsetLeft
  195. const walk = (x - startX.value) * 2 // 滚动速度
  196. middleBox.value.scrollLeft = scrollLeft.value - walk
  197. }
  198. // 鼠标松开事件处理函数
  199. const handleMouseUp = () => {
  200. isDragging.value = false
  201. }
  202. // 鼠标离开事件处理函数
  203. const handleMouseLeave = () => {
  204. isDragging.value = false
  205. }
  206. // 上一页
  207. const prevSlide = () => {
  208. if (currentIndex.value > 0) {
  209. currentIndex.value = Math.max(0, currentIndex.value - 3)
  210. // 滚动到对应位置
  211. if (middleBox.value) {
  212. const scrollAmount = middleBox.value.clientWidth * 0.75
  213. middleBox.value.scrollTo({
  214. left: Math.max(0, middleBox.value.scrollLeft - scrollAmount),
  215. behavior: 'smooth'
  216. })
  217. }
  218. }
  219. }
  220. // 下一页
  221. const nextSlide = () => {
  222. if (currentIndex.value < specificCourses.length - 3) {
  223. currentIndex.value = Math.min(specificCourses.length - 3, currentIndex.value + 3)
  224. // 滚动到对应位置
  225. if (middleBox.value) {
  226. const scrollAmount = middleBox.value.clientWidth * 0.75
  227. middleBox.value.scrollTo({
  228. left: middleBox.value.scrollLeft + scrollAmount,
  229. behavior: 'smooth'
  230. })
  231. }
  232. }
  233. }
  234. // 获取middleBox元素的引用
  235. const middleBox = ref(null)
  236. // 组件挂载时获取路由参数设置标题和课程ID
  237. onMounted(() => {
  238. if (localStorage.getItem("blocklyDataScope")) {
  239. blocklyDataScope.value = localStorage.getItem("blocklyDataScope").split(",");
  240. }
  241. const title = route.query.courseTitle
  242. if (title) {
  243. pageTitle.value = title
  244. }
  245. const id = route.query.categoryId
  246. if (id) {
  247. categoryId.value = id
  248. // 获取到categoryId后调用getTypeByThemeId接口
  249. fetchTopicList()
  250. }
  251. })
  252. // 跳转到课程详情页面
  253. const goToProgrammingList = (courseType, index) => {
  254. // 检查是否禁用
  255. if (courseType.isDisabled) {
  256. Message().notifyWarning('您的账号并未开放此课程!', true)
  257. return
  258. }
  259. // 设置当前选中项
  260. activeButton.value = index;
  261. // 跳转ProgrammingCourset页面,并传递课程信息作为参数
  262. router.push({
  263. path: '/programmingCourset',
  264. query: {
  265. courseTitle: courseType.title,
  266. courseIndex: index,
  267. typeId: courseType.id, // 当前类型的id,避免与课程ID混淆
  268. originalCourseId: categoryId.value, // 原始的课程ID
  269. originalCourseTitle: pageTitle.value // 原始的课程标题
  270. }
  271. })
  272. }
  273. // 禁用视频
  274. const disableBlockly = (blocklyId = categoryId.value) => {
  275. // 未配置课程权限,不禁用视频
  276. if (!blocklyDataScope.value || blocklyDataScope.value.length === 0) {
  277. return false
  278. }
  279. //配置了课程权限,且视频id不在权限列表中
  280. isDisabled.value = !blocklyDataScope.value.some(item => Number(item) === blocklyId)
  281. // if (isDisabled.value) {
  282. // Message().notifyWarning('您的账号并未开放此课程!', true)
  283. // return isDisabled.value;
  284. // }
  285. return isDisabled.value;
  286. }
  287. </script>
  288. <style scoped lang="scss">
  289. @use 'sass:math';
  290. // 定义rpx转换函数
  291. @function rpx($px) {
  292. @return math.div($px, 750) * 100vw;
  293. }
  294. .programming-content{
  295. position: fixed;
  296. top: 0;
  297. left: 0;
  298. right: 0;
  299. bottom: 0;
  300. background-image: url('@/assets/programming/list_bg02.png');
  301. background-size: cover;
  302. background-position: center;
  303. background-repeat: no-repeat;
  304. display: flex;
  305. flex-direction: column;
  306. }
  307. .top-box {
  308. height: 20%;
  309. display: flex;
  310. }
  311. .top-left-box,
  312. .top-right-box {
  313. flex: 1;
  314. height: 50%;
  315. border-radius: rpx(5);
  316. display: flex;
  317. align-items: center;
  318. justify-content: center;
  319. }
  320. .top-center-box {
  321. flex: 2;
  322. border-radius: rpx(5);
  323. display: flex;
  324. align-items: center;
  325. justify-content: center;
  326. }
  327. .top-left-inner-box,
  328. .top-center-inner-box,
  329. .top-right-inner-box {
  330. width: 100%;
  331. height: 100%;
  332. }
  333. .top-center-inner-box{
  334. background-size: 70%;
  335. background-position: 50% 80%;
  336. background-repeat: no-repeat;
  337. display: flex; /* 使用flex布局 */
  338. align-items: center; /* 垂直居中 */
  339. justify-content: center; /* 水平居中 */
  340. span{
  341. font-size: rpx(17);
  342. color: white;
  343. }
  344. }
  345. .top-left-inner-box{
  346. display: flex;
  347. align-items: center; /* 垂直居中对齐 */
  348. .left-icon{
  349. font-size: rpx(14);
  350. color: white;
  351. padding-left: rpx(20);
  352. cursor: pointer;
  353. }
  354. .left-text{
  355. font-size: rpx(14);
  356. color: white;
  357. padding-left: rpx(10);
  358. cursor: pointer;
  359. }
  360. }
  361. .middle-wrapper {
  362. height: 60%;
  363. overflow: hidden; /* 溢出隐藏 */
  364. position: relative;
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. }
  369. .middle-box {
  370. min-width: rpx(660); /* 固定最小宽度 */
  371. height: 100%;
  372. margin: 0 rpx(50);
  373. overflow-x: auto; /* 水平滚动条 */
  374. overflow-y: hidden; /* 取消上下滚动 */
  375. white-space: nowrap; /* 防止元素换行 */
  376. scroll-behavior: smooth; /* 平滑滚动效果 */
  377. -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
  378. position: relative;
  379. flex: 1;
  380. }
  381. .middle-inner-box{
  382. width: rpx(130); /* 设置固定宽度 */
  383. height: 100%; /* 设置固定高度 */
  384. position: relative;
  385. cursor: pointer; /* 显示可抓取图标 */
  386. user-select: none; /* 禁止文本选择 */
  387. display: inline-block; /* 水平排列 */
  388. margin-right: rpx(20);
  389. margin-left: rpx(15);
  390. vertical-align: middle; /* 垂直居中对齐 */
  391. }
  392. /* 鼠标按下时的光标样式 */
  393. .middle-inner-box:active {
  394. cursor: grabbing;
  395. }
  396. /* 隐藏滚动条 */
  397. .middle-box::-webkit-scrollbar {
  398. height: rpx(0);
  399. }
  400. .new-white-box {
  401. width: 100%;
  402. height: 65%;
  403. margin-top: rpx(20);
  404. background-color: white;
  405. border-radius: rpx(15);
  406. display: flex;
  407. flex-direction: column;
  408. transition: all 0.3s ease; /* 过渡效果 */
  409. position: relative;
  410. }
  411. /* 小三角效果 */
  412. .new-white-box::after {
  413. content: '';
  414. position: absolute;
  415. bottom: -rpx(8);
  416. left: 50.5%;
  417. transform: translateX(-50%);
  418. width: 0;
  419. height: 0;
  420. top: rpx(150);
  421. border-left: rpx(10) solid transparent;
  422. border-right: rpx(10) solid transparent;
  423. border-top: rpx(10) solid white;
  424. }
  425. /* 鼠标悬浮/选中时的放大效果 */
  426. .new-white-box:hover:not(.disabled),
  427. .new-white-box.active:not(.disabled) {
  428. transform: scale(1.1); /* 放大1.1倍 */
  429. z-index: 10; /* 确保放大时在顶层显示 */
  430. border: rpx(5) solid #D0D7F7; /* 边框效果 */
  431. box-shadow: 0 0 rpx(10) rgba(0, 0, 0, 0.5);
  432. }
  433. /* 禁用状态样式 */
  434. .new-white-box.disabled {
  435. cursor: not-allowed;
  436. filter: grayscale(0%); /* 改为100%黑白色 */
  437. pointer-events: none;
  438. position: relative;
  439. }
  440. /* 禁用状态下的文字颜色 */
  441. .new-white-box.disabled .box-title {
  442. color: #676666;
  443. }
  444. /* 禁用状态下的遮挡层 */
  445. .new-white-box.disabled::before {
  446. content: '';
  447. position: absolute;
  448. top: 0;
  449. left: 0;
  450. width: 100%;
  451. height: 100%;
  452. background-color: rgba(0, 0, 0, 0.3);
  453. background-image: var(--lock-image);
  454. background-size: 40%;
  455. background-position: center;
  456. background-repeat: no-repeat;
  457. border-radius: rpx(15);
  458. z-index: 2;
  459. }
  460. /* 小三角效果 */
  461. .new-white-box.disabled::after {
  462. top: rpx(150);
  463. opacity: 0.5;
  464. }
  465. /* 背景图容器 */
  466. .bg-image-container {
  467. width: 100%;
  468. height: 85%;
  469. background-size: 105%;
  470. background-position: center;
  471. background-repeat: no-repeat;
  472. position: relative;
  473. }
  474. /* star图标样式 */
  475. .star-icon {
  476. position: absolute;
  477. top: rpx(10);
  478. width: rpx(20);
  479. height: rpx(20);
  480. background-size: contain;
  481. background-position: center;
  482. background-repeat: no-repeat;
  483. z-index: 1;
  484. }
  485. /* 文字容器 */
  486. .text-container {
  487. width: 100%;
  488. height: 15%;
  489. display: flex;
  490. line-height: rpx(10);
  491. justify-content: center;
  492. }
  493. /* 标题样式 */
  494. .box-title {
  495. color: #333;
  496. text-align: center;
  497. font-size: rpx(11);
  498. color: #45300b;
  499. }
  500. /* unlock盒子样式 */
  501. .unlock-box {
  502. position: absolute;
  503. width: 100%;
  504. height: 30%;
  505. background-size: contain;
  506. background-position: center;
  507. background-repeat: no-repeat;
  508. left: 51%;
  509. transform: translateX(-50%);
  510. top: rpx(148);
  511. z-index: 5;
  512. }
  513. .bottom-box {
  514. height: 20%;
  515. display: flex;
  516. align-items: center;
  517. justify-content: center;
  518. }
  519. .line-container {
  520. width: 65%;
  521. position: relative;
  522. display: flex;
  523. align-items: center;
  524. justify-content: space-between;
  525. }
  526. .bold-line {
  527. position: absolute;
  528. width: 100%;
  529. height: rpx(5);
  530. background-color: rgba(37, 115, 188, 0.5);
  531. border-radius: rpx(3);
  532. }
  533. .circle-button {
  534. width: rpx(7);
  535. height: rpx(7);
  536. border-radius: 50%;
  537. background-color: rgba(255, 255, 255);
  538. border: rpx(1) solid #00c1fc;
  539. display: flex;
  540. align-items: center;
  541. justify-content: center;
  542. cursor: pointer;
  543. transition: all 0.3s ease;
  544. z-index: 1;
  545. }
  546. .circle-button:hover {
  547. background-color: white;
  548. transform: scale(1.5); // 放大效果
  549. }
  550. .circle-button.active {
  551. background-color: #f9df04;
  552. border: rpx(1.5) solid #00c1fc;
  553. color: white;
  554. transform: scale(2);
  555. box-shadow: 0 0 rpx(10) rgba(0, 0, 0, 0.5);
  556. }
  557. /* 轮播图按钮样式 */
  558. .carousel-btn {
  559. position: absolute;
  560. top: 50%;
  561. transform: translateY(-50%);
  562. width: rpx(30);
  563. height: rpx(30);
  564. background-color: rgba(255, 255, 255, 0.8);
  565. border-radius: 50%;
  566. display: flex;
  567. align-items: center;
  568. justify-content: center;
  569. cursor: pointer;
  570. z-index: 10;
  571. box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
  572. transition: all 0.3s ease;
  573. }
  574. .carousel-btn:hover:not(.disabled-btn) {
  575. background-color: rgba(255, 255, 255, 1);
  576. transform: translateY(-50%) scale(1.1);
  577. }
  578. .carousel-btn:disabled,
  579. .disabled-btn {
  580. opacity: 0.5;
  581. cursor: not-allowed;
  582. background-color: rgba(200, 200, 200, 0.7);
  583. box-shadow: none;
  584. }
  585. .disabled-icon {
  586. color: #ccc !important;
  587. filter: grayscale(100%);
  588. opacity: 0.6;
  589. }
  590. .prev-btn {
  591. left: 2%;
  592. }
  593. .next-btn {
  594. right: 2%;
  595. }
  596. .btn-icon {
  597. font-size: rpx(15);
  598. color: #333;
  599. }
  600. </style>