|
@@ -27,18 +27,28 @@
|
|
|
|
|
|
|
|
<!-- 课程部分 -->
|
|
<!-- 课程部分 -->
|
|
|
<div class="lower-box" v-if="!showVideo">
|
|
<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">
|
|
<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>
|
|
<el-icon class="btn-icon" :class="{ 'disabled-icon': currentIndex === 0 }"><ArrowLeftBold /></el-icon>
|
|
|
</div>
|
|
</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
|
|
<div
|
|
|
- v-for="(item, index) in displayItems"
|
|
|
|
|
|
|
+ v-for="(item) in courseItems"
|
|
|
:key="item.id"
|
|
:key="item.id"
|
|
|
- :class="getPositionClass(index)"
|
|
|
|
|
|
|
+ class="slide-item"
|
|
|
@click="handleCourseItemClick(item)"
|
|
@click="handleCourseItemClick(item)"
|
|
|
>
|
|
>
|
|
|
<div class="box-content">
|
|
<div class="box-content">
|
|
@@ -111,6 +121,17 @@ const resData = ref([])
|
|
|
// 轮播图当前索引
|
|
// 轮播图当前索引
|
|
|
const currentIndex = ref(0)
|
|
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) => {
|
|
const getStarCount = (contentType) => {
|
|
|
if (contentType === 'video') {
|
|
if (contentType === 'video') {
|
|
@@ -118,34 +139,136 @@ const getStarCount = (contentType) => {
|
|
|
} else if (contentType === 'blockly') {
|
|
} else if (contentType === 'blockly') {
|
|
|
return 3; // blockly类型渲染3个星星
|
|
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 = () => {
|
|
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 = () => {
|
|
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(() => {
|
|
onMounted(() => {
|
|
|
// 检查路由参数中是否有courseTitle
|
|
// 检查路由参数中是否有courseTitle
|
|
@@ -156,11 +279,9 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
// 获取当前类型ID
|
|
// 获取当前类型ID
|
|
|
typeId.value = route.query.typeId
|
|
typeId.value = route.query.typeId
|
|
|
-
|
|
|
|
|
// 保存原始的课程ID和标题,返回时使用
|
|
// 保存原始的课程ID和标题,返回时使用
|
|
|
originalCourseId.value = route.query.originalCourseId
|
|
originalCourseId.value = route.query.originalCourseId
|
|
|
originalCourseTitle.value = route.query.originalCourseTitle
|
|
originalCourseTitle.value = route.query.originalCourseTitle
|
|
|
-
|
|
|
|
|
// 获取到topicId后,再获取课程列表getBlocklyByTypeId接口
|
|
// 获取到topicId后,再获取课程列表getBlocklyByTypeId接口
|
|
|
if (typeId.value) {
|
|
if (typeId.value) {
|
|
|
getBlocklyByTypeId(typeId.value).then(res => {
|
|
getBlocklyByTypeId(typeId.value).then(res => {
|
|
@@ -195,6 +316,8 @@ onMounted(() => {
|
|
|
|
|
|
|
|
// 处理课程项点击事件
|
|
// 处理课程项点击事件
|
|
|
const handleCourseItemClick = (item) => {
|
|
const handleCourseItemClick = (item) => {
|
|
|
|
|
+ // 如果在拖动过程中,则不触发点击事件
|
|
|
|
|
+ if (hasMoved.value) return
|
|
|
if (item.contentType === 'video' || item.contentType === 'blockly') {
|
|
if (item.contentType === 'video' || item.contentType === 'blockly') {
|
|
|
showVideo.value = true
|
|
showVideo.value = true
|
|
|
// 查找并保存完整的课程数据
|
|
// 查找并保存完整的课程数据
|
|
@@ -239,6 +362,7 @@ const goBackIndex = () => {
|
|
|
background-repeat: no-repeat;
|
|
background-repeat: no-repeat;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ user-select: none; /* 禁止文本选择 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.top-box {
|
|
.top-box {
|
|
@@ -331,58 +455,74 @@ const goBackIndex = () => {
|
|
|
.content-box {
|
|
.content-box {
|
|
|
min-width: rpx(660);
|
|
min-width: rpx(660);
|
|
|
height: 100%;
|
|
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;
|
|
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);
|
|
border-radius: rpx(40);
|
|
|
background-color: rgba(255, 255, 255);
|
|
background-color: rgba(255, 255, 255);
|
|
|
- display: flex;
|
|
|
|
|
|
|
+ display: inline-flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
|
|
box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
|
|
|
transition: transform 0.3s ease;
|
|
transition: transform 0.3s ease;
|
|
|
cursor: pointer;
|
|
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%;
|
|
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 {
|
|
.box-image {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
@@ -452,9 +590,10 @@ const goBackIndex = () => {
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
- z-index: 10;
|
|
|
|
|
|
|
+ z-index: 100; /* 提高按钮层级确保始终在最上层 */
|
|
|
box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
|
|
box-shadow: 0 rpx(5) rpx(10) rgba(0, 0, 0, 0.2);
|
|
|
transition: all 0.3s ease;
|
|
transition: all 0.3s ease;
|
|
|
|
|
+ pointer-events: all;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.carousel-btn:hover:not(.disabled-btn) {
|
|
.carousel-btn:hover:not(.disabled-btn) {
|