Bladeren bron

修改课程列表页面

丸子 4 maanden geleden
bovenliggende
commit
b629a3dd35

BIN
src/assets/programming/track01.png


+ 206 - 67
src/views/programming/ProgrammingCourset.vue

@@ -27,18 +27,28 @@
 
     <!-- 课程部分 -->
     <div class="lower-box" v-if="!showVideo">
-        <!-- 轨道图片 -->
-         <img src="@/assets/programming/track01.png" class="background-img" alt="背景图" />
         <!-- 左切换按钮 -->
         <div v-if="!showVideo && courseItems.length > 3" class="carousel-btn prev-btn" @click="prevSlide" :class="{ 'disabled-btn': currentIndex === 0 }" :disabled="currentIndex === 0">
           <el-icon class="btn-icon" :class="{ 'disabled-icon': currentIndex === 0 }"><ArrowLeftBold /></el-icon>
         </div>
-        <div class="content-box">
+        <div 
+          class="content-box"
+          ref="contentBox"
+          @mousedown="handleMouseDown"
+          @mousemove="handleMouseMove"
+          @mouseup="handleMouseUp"
+          @mouseleave="handleMouseLeave"
+          @touchstart="handleTouchStart"
+          @touchmove="handleTouchMove"
+          @touchend="handleTouchEnd"
+          @touchcancel="handleTouchEnd"
+          @scroll="handleScroll"
+        >
           <!-- 动态渲染课程内容 -->
           <div 
-            v-for="(item, index) in displayItems" 
+            v-for="(item) in courseItems" 
             :key="item.id"
-            :class="getPositionClass(index)"
+            class="slide-item"
             @click="handleCourseItemClick(item)"
           >
             <div class="box-content">
@@ -111,6 +121,17 @@ const resData = ref([])
 // 轮播图当前索引
 const currentIndex = ref(0)
 
+// 拖动相关变量
+const isDragging = ref(false)
+const startX = ref(0)
+const scrollLeft = ref(0)
+const hasMoved = ref(false) // 标记是否发生了移动
+const touchStartX = ref(0)
+const touchStartTime = ref(0)
+
+// 获取contentBox元素的引用
+const contentBox = ref(null)
+
 // 根据内容类型获取星星数量
 const getStarCount = (contentType) => {
   if (contentType === 'video') {
@@ -118,34 +139,136 @@ const getStarCount = (contentType) => {
   } else if (contentType === 'blockly') {
     return 3; // blockly类型渲染3个星星
   }
-  return 0; // 其他类型默认不渲染星星
-}
-
-// 显示的项目(每次显示3个)
-const displayItems = computed(() => {
-  return courseItems.value.slice(currentIndex.value, currentIndex.value + 3)
-})
-
-// 获取位置类
-const getPositionClass = (index) => {
-  const positionClasses = ['left-content-box', 'center-content-box', 'right-content-box']
-  return positionClasses[index]
 }
 
 // 上一页
 const prevSlide = () => {
-  if (currentIndex.value > 0) {
-    currentIndex.value = Math.max(0, currentIndex.value - 3)
+  if (contentBox.value) {
+    const scrollAmount = contentBox.value.clientWidth * 0.75
+    contentBox.value.scrollTo({
+      left: Math.max(0, contentBox.value.scrollLeft - scrollAmount),
+      behavior: 'smooth'
+    })
   }
 }
 
 // 下一页
 const nextSlide = () => {
-  if (currentIndex.value < courseItems.value.length - 3) {
-    currentIndex.value = Math.min(courseItems.value.length - 3, currentIndex.value + 3)
+  if (contentBox.value) {
+    const scrollAmount = contentBox.value.clientWidth * 0.75
+    contentBox.value.scrollTo({
+      left: contentBox.value.scrollLeft + scrollAmount,
+      behavior: 'smooth'
+    })
   }
 }
 
+// 鼠标按下事件处理函数
+const handleMouseDown = (e) => {
+  // 拖拽状态为true
+  isDragging.value = true
+  // 初始化移动状态为false
+  hasMoved.value = false
+  // 计算鼠标在容器内的相对X坐标的位置  e.pageX鼠标相对于整个页面的X坐标
+  startX.value = e.pageX - contentBox.value.offsetLeft
+  // 记录当前容器的滚动位置
+  scrollLeft.value = contentBox.value.scrollLeft
+  e.stopPropagation() // 阻止事件冒泡 防止事件继续向上传播到父元素
+}
+
+// 鼠标移动事件处理函数
+const handleMouseMove = (e) => {
+  if (!isDragging.value) return
+  // 标记已发生移动
+  hasMoved.value = true
+  // 阻止默认行为和冒泡
+  e.preventDefault()
+  e.stopPropagation()
+  const x = e.pageX - contentBox.value.offsetLeft
+  const walk = (x - startX.value) * 2 // 滚动速度
+  contentBox.value.scrollLeft = scrollLeft.value - walk
+}
+
+// 鼠标松开事件处理函数
+const handleMouseUp = (e) => {
+  e.stopPropagation()
+  isDragging.value = false
+  // 延迟重置hasMoved,确保点击事件可以正常判断
+  setTimeout(() => {
+    hasMoved.value = false
+  }, 100)
+}
+
+// 鼠标离开事件处理函数
+const handleMouseLeave = () => {
+  isDragging.value = false
+  hasMoved.value = false
+}
+
+// 触摸开始事件处理函数(支持移动设备)
+const handleTouchStart = (e) => {
+  const touch = e.touches[0]
+  isDragging.value = true
+  hasMoved.value = false
+  touchStartX.value = touch.clientX
+  touchStartTime.value = Date.now()
+  scrollLeft.value = contentBox.value.scrollLeft
+  e.stopPropagation()
+}
+
+// 触摸移动事件处理函数
+const handleTouchMove = (e) => {
+  if (!isDragging.value) return
+  
+  const touch = e.touches[0]
+  const diffX = touchStartX.value - touch.clientX
+  
+  // 判断是否水平滑动
+  if (Math.abs(diffX) > 5) {
+    hasMoved.value = true
+    // 阻止默认行为和冒泡
+    e.preventDefault()
+    e.stopPropagation()
+    // 设置滚动位置
+    contentBox.value.scrollLeft = scrollLeft.value + diffX
+  }
+}
+
+// 触摸结束事件处理函数(支持移动设备)
+const handleTouchEnd = (e) => {
+  e.stopPropagation()
+  isDragging.value = false
+  // 延迟重置hasMoved
+  setTimeout(() => {
+    hasMoved.value = false
+  }, 100)
+}
+
+// 节流函数 - 限制函数在一段时间内只能执行一次
+const throttle = (func, limit) => {
+  let inThrottle
+  return function() {
+    const args = arguments
+    const context = this
+    if (!inThrottle) {
+      func.apply(context, args)
+      inThrottle = true
+      setTimeout(() => {
+        inThrottle = false
+      }, limit)
+    }
+  }
+}
+
+// 优化的滚动事件处理函数 
+let animationFrameId = null
+const handleScroll = throttle(() => {
+  // 取消之前的动画帧请求
+  if (animationFrameId) {
+    cancelAnimationFrame(animationFrameId)
+  }
+}, 16) // 约60fps的频率
+
 // 组件挂载时获取路由参数设置标题
 onMounted(() => {
   // 检查路由参数中是否有courseTitle
@@ -156,11 +279,9 @@ onMounted(() => {
   }
   // 获取当前类型ID
   typeId.value = route.query.typeId
-  
   // 保存原始的课程ID和标题,返回时使用
   originalCourseId.value = route.query.originalCourseId
   originalCourseTitle.value = route.query.originalCourseTitle
-  
   // 获取到topicId后,再获取课程列表getBlocklyByTypeId接口
   if (typeId.value) {
     getBlocklyByTypeId(typeId.value).then(res => {
@@ -195,6 +316,8 @@ onMounted(() => {
 
 // 处理课程项点击事件
 const handleCourseItemClick = (item) => {
+  // 如果在拖动过程中,则不触发点击事件
+  if (hasMoved.value) return
   if (item.contentType === 'video' || item.contentType === 'blockly') {
     showVideo.value = true
     // 查找并保存完整的课程数据
@@ -239,6 +362,7 @@ const goBackIndex = () => {
   background-repeat: no-repeat;
   display: flex;
   flex-direction: column;
+  user-select: none; /* 禁止文本选择 */
 }
 
 .top-box {
@@ -331,58 +455,74 @@ const goBackIndex = () => {
 .content-box {
   min-width: rpx(660);
   height: 100%;
-  display: flex;
-  justify-content: space-between;
+  overflow-x: auto; /* 水平滚动条 */
+  overflow-y: hidden; /* 取消上下滚动 */
+  white-space: nowrap; /* 防止元素换行 */
+  scroll-behavior: smooth; /* 平滑滚动效果 */
+  -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
   position: relative;
-  overflow: hidden;
+  flex: 1;
+  cursor: grab; /* 显示可抓取图标 */
+  z-index: 2; 
+  padding: 0  rpx(30);
+  /* 设置背景图 */
+  background-image: url('@/assets/programming/track01.png');
+  background-size: 150% rpx(350);
+  background-position: left calc(-1 * rpx(50));;
+  background-repeat: no-repeat;
+  background-attachment: local; /* 背景图随内容滚动 */
 }
 
-.background-img {
-  position: absolute;
-  width: 150%;
-  height: 60%;
-  left:  -10%;
-  top: 25%;
-  object-fit: cover;
-  z-index: 1;
+/* 鼠标按下时的光标样式 */
+.content-box:active {
+  cursor: grabbing;
 }
 
-.left-content-box, .center-content-box, .right-content-box {
-  flex: 1;
-  height: 40%; /* 高度缩小 */
-  margin: 0 rpx(10);
+/* 隐藏滚动条但保持滚动功能 */
+.content-box::-webkit-scrollbar {
+  display: none;
+}
+
+.content-box {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+/* 背景图样式已整合到content-box中,此处样式已移除 */
+
+.slide-item {
+  width: rpx(140); /* 设置固定宽度 */
+  height: rpx(120); /* 高度设置 */
+  margin: rpx(80) rpx(35);
   border-radius: rpx(40);
   background-color: rgba(255, 255, 255);
-  display: flex;
+  display: inline-flex;
   align-items: center;
   justify-content: center;
   box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
   transition: transform 0.3s ease;
   cursor: pointer;
-  z-index: 2; /* 确保内容在背景图上方 */
+  z-index: 2; /* 内容在背景图上方 */
+  vertical-align: middle;
+  position: relative;
 }
-
-/* 位置调整 */
-.left-content-box {
-  position: absolute;
-  left: 5%;
-  top: 10%; /* 偏上 */
-  width: calc(20% - rpx(10));
+/* 奇数项在上层 */
+.slide-item:nth-child(odd) {
+  transform: translateY(-50%);
 }
-
-.center-content-box {
-  position: absolute;
-  left: 48%;
-  transform: translateX(-50%);
-  bottom: 15%; /* 偏下 */
-  width: calc(20% - rpx(10));
+/* 偶数项在下层 */
+.slide-item:nth-child(even) {
+  transform: translateY(50%);
+}
+/* 鼠标悬停放大效果 - 在保持原有垂直位置的基础上放大 */
+.slide-item:nth-child(odd):hover {
+  transform: translateY(-50%) scale(1.1);
+  z-index: 10; /* 确保放大时在顶层显示 */
 }
 
-.right-content-box {
-  position: absolute;
-  right: 5%;
-  top: 10%; /* 偏上 */
-  width: calc(20% - rpx(10));
+.slide-item:nth-child(even):hover {
+  transform: translateY(50%) scale(1.1);
+  z-index: 10; /* 确保放大时在顶层显示 */
 }
 
 /* 内容样式 */
@@ -395,13 +535,11 @@ const goBackIndex = () => {
   width: 100%;
 }
 
-/* 鼠标悬停放大效果 */
-.left-content-box:hover,
-.right-content-box:hover{
-  transform: scale(1.1);
-}
-.center-content-box:hover {
-  transform: translateX(-50%) scale(1.1);
+
+
+/* 鼠标按下时的光标样式 */
+.slide-item:active {
+  cursor: grabbing;
 }
 .box-image {
   width: 100%;
@@ -452,9 +590,10 @@ const goBackIndex = () => {
   align-items: center;
   justify-content: center;
   cursor: pointer;
-  z-index: 10;
+  z-index: 100; /* 提高按钮层级确保始终在最上层 */
   box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
   transition: all 0.3s ease;
+  pointer-events: all;
 }
 
 .carousel-btn:hover:not(.disabled-btn) {

+ 99 - 15
src/views/programming/ProgrammingList.vue

@@ -34,29 +34,34 @@
               @mousemove="handleMouseMove"
               @mouseup="handleMouseUp"
               @mouseleave="handleMouseLeave"
+              @touchstart="handleTouchStart"
+              @touchmove="handleTouchMove"
+              @touchend="handleTouchEnd"
+              @touchcancel="handleTouchEnd"
             >
               <div
                 v-for="(courseType, index) in specificCourses"
                 :key="index"
                 class="middle-inner-box"
-                @click="goToProgrammingList(courseType, index)"
+                @click="handleItemClick(courseType, index)"
               >
                 <div class="new-white-box"
                      :class="{ 'active': activeButton === index, 'disabled': courseType.isDisabled }"
-                     @click="goToProgrammingList(courseType, index)">
+                     @click.stop="handleItemClick(courseType, index)">
                     <!-- 列表封面图 -->
                   <div class="bg-image-container" :style="{ backgroundImage: `url(${courseType.bgImage})` }">
                     <!-- 星星图标 -->
-                    <div
-                      v-if="!courseType.isDisabled"
-                      v-for="starIndex in 5"
-                      :key="starIndex"
-                      class="star-icon"
-                      :style="{
-                        backgroundImage: `url(${starIndex <= courseType.progress ? star01Image : star02Image})`,
-                        left: `${30 + (starIndex - 1) * 30}px`
-                      }"
-                    ></div>
+                    <template v-if="!courseType.isDisabled">
+                      <div
+                        v-for="starIndex in 5"
+                        :key="starIndex"
+                        class="star-icon"
+                        :style="{
+                          backgroundImage: `url(${starIndex <= courseType.progress ? star01Image : star02Image})`,
+                          left: `${30 + (starIndex - 1) * 30}px`
+                        }"
+                      ></div>
+                    </template>
                   </div>
                   <div class="text-container">
                     <div class="box-title">
@@ -198,31 +203,99 @@ watch(activeButton, (newIndex) => {
 const isDragging = ref(false)
 const startX = ref(0)
 const scrollLeft = ref(0)
+const hasMoved = ref(false) // 标记是否发生了移动
+const touchStartX = ref(0)
+const touchStartTime = ref(0)
 
 // 鼠标按下事件处理函数
 const handleMouseDown = (e) => {
   isDragging.value = true
+  hasMoved.value = false
   startX.value = e.pageX - middleBox.value.offsetLeft
   scrollLeft.value = middleBox.value.scrollLeft
+  e.stopPropagation() // 阻止事件冒泡
 }
 
 // 鼠标移动事件处理函数
 const handleMouseMove = (e) => {
   if (!isDragging.value) return
+  
+  // 标记已发生移动
+  hasMoved.value = true
+  
+  // 阻止默认行为和冒泡
   e.preventDefault()
+  e.stopPropagation()
+  
   const x = e.pageX - middleBox.value.offsetLeft
   const walk = (x - startX.value) * 2 // 滚动速度
   middleBox.value.scrollLeft = scrollLeft.value - walk
 }
 
 // 鼠标松开事件处理函数
-const handleMouseUp = () => {
+const handleMouseUp = (e) => {
+  e.stopPropagation()
   isDragging.value = false
+  // 延迟重置hasMoved,确保点击事件可以正常判断
+  setTimeout(() => {
+    hasMoved.value = false
+  }, 100)
 }
 
 // 鼠标离开事件处理函数
 const handleMouseLeave = () => {
   isDragging.value = false
+  hasMoved.value = false
+}
+
+// 触摸开始事件处理函数(支持移动设备)
+const handleTouchStart = (e) => {
+  const touch = e.touches[0]
+  isDragging.value = true
+  hasMoved.value = false
+  touchStartX.value = touch.clientX
+  touchStartTime.value = Date.now()
+  scrollLeft.value = middleBox.value.scrollLeft
+  e.stopPropagation()
+}
+
+// 触摸移动事件处理函数(支持移动设备)
+const handleTouchMove = (e) => {
+  if (!isDragging.value) return
+  
+  const touch = e.touches[0]
+  const diffX = touchStartX.value - touch.clientX
+  
+  // 判断是否为有效的水平滑动
+  if (Math.abs(diffX) > 5) {
+    hasMoved.value = true
+    
+    // 阻止默认行为和冒泡
+    e.preventDefault()
+    e.stopPropagation()
+    
+    // 设置滚动位置
+    middleBox.value.scrollLeft = scrollLeft.value + diffX
+  }
+}
+
+// 触摸结束事件处理函数(支持移动设备)
+const handleTouchEnd = (e) => {
+  e.stopPropagation()
+  isDragging.value = false
+  // 延迟重置hasMoved
+  setTimeout(() => {
+    hasMoved.value = false
+  }, 100)
+}
+
+// 处理项目点击事件
+const handleItemClick = (courseType, index) => {
+  // 如果在拖动过程中,则不触发点击事件
+  if (hasMoved.value) return
+  
+  // 调用原始的goToProgrammingList函数
+  goToProgrammingList(courseType, index)
 }
 
 // 上一页
@@ -417,6 +490,12 @@ const disableBlockly = (blocklyId = categoryId.value) => {
   -webkit-overflow-scrolling: touch; /* 触摸设备支持 */
   position: relative;
   flex: 1;
+  cursor: grab; /* 显示可抓取图标 */
+}
+
+/* 鼠标按下时的光标样式 */
+.middle-box:active {
+  cursor: grabbing;
 }
 
 .middle-inner-box{
@@ -436,9 +515,14 @@ const disableBlockly = (blocklyId = categoryId.value) => {
   cursor: grabbing;
 }
 
-/* 隐藏滚动条 */
+/* 隐藏滚动条但保持滚动功能 */
 .middle-box::-webkit-scrollbar {
-  height: rpx(0);
+  display: none;
+}
+
+.middle-box {
+  -ms-overflow-style: none;
+  scrollbar-width: none;
 }