FeaturedCoursesSection.vue
6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<template>
<div v-if="courses.length" class="mb-6">
<div class="px-4 mb-2">
<h3 class="font-medium">精选课程</h3>
</div>
<div class="relative">
<div
ref="carouselRef"
class="flex overflow-x-scroll snap-x snap-mandatory"
style="scrollbar-width: none; -ms-overflow-style: none;"
>
<div
v-for="(course, index) in courses"
:key="course.id"
class="flex-shrink-0 w-full snap-center px-4"
>
<div class="relative rounded-xl overflow-hidden shadow-lg h-48">
<img
:src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
:alt="course.title"
class="w-full h-full object-cover"
/>
<div
v-if="course.is_buy"
class="absolute top-0 left-0 bg-orange-500 text-white text-xs px-2 py-1 rounded-br-lg font-medium"
style="background-color: rgba(249, 115, 22, 0.85)"
>
已购
</div>
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-black/20 to-black/60 flex flex-col justify-end p-4">
<div v-if="course.category" class="bg-amber-500/90 text-white px-2 py-1 rounded-full text-xs font-medium inline-block w-fit mb-1">
{{ course.category }}
</div>
<h2 class="text-2xl font-bold text-white drop-shadow-md">{{ course.title }}</h2>
<p class="text-white/90 text-sm drop-shadow-sm mb-1">{{ course.subtitle }}</p>
<div class="flex justify-between items-center">
<div class="flex items-center">
<div class="flex">
<svg
v-for="i in 5"
:key="i"
xmlns="http://www.w3.org/2000/svg"
:class="[`h-4 w-4`, i <= course.comment_score ? 'text-amber-400' : 'text-gray-300']"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</div>
<span class="text-white text-xs ml-1">{{ course.comment_count }}人评</span>
</div>
<router-link
:to="`/courses/${course.id}`"
class="bg-white/90 text-green-600 px-3 py-1 rounded-full text-xs font-medium"
>
{{ course.price === '0.00' ? '免费学习' : '立即学习' }}
</router-link>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-4">
<button
v-for="(_, index) in courses.slice(0, 4)"
:key="index"
@click="scroll_to_slide(index)"
:class="[
'w-2 h-2 mx-1 rounded-full',
current_slide === index ? 'bg-green-600' : 'bg-gray-300'
]"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getCourseListAPI } from '@/api/course'
const courses = ref([])
const carouselRef = ref(null)
const current_slide = ref(0)
let carousel_interval
/**
* @description 获取精选课程数据
* @returns {Promise<void>}
*/
const fetch_courses = async () => {
const res = await getCourseListAPI({
sn: 'JXKC',
limit: 4
})
if (res && res.code) {
courses.value = Array.isArray(res.data) ? res.data : []
} else {
courses.value = []
}
}
/**
* @description 根据滚动位置同步当前轮播索引
* @returns {void}
*/
const sync_current_slide = () => {
const container = carouselRef.value
const total = courses.value.slice(0, 4).length
if (!container || total === 0) return
const slide_width = container.offsetWidth
if (!slide_width) return
const raw_index = container.scrollLeft / slide_width
const idx = Math.round(raw_index)
const bounded = Math.min(Math.max(idx, 0), total - 1)
current_slide.value = bounded
}
/**
* @description 轮播滚动事件处理(被动监听)
* @returns {void}
*/
const handle_carousel_scroll = () => {
sync_current_slide()
}
/**
* @description 轮播图控制:滚动到指定位置
* @param {number} index 轮播索引
* @returns {void}
*/
const scroll_to_slide = (index) => {
const container = carouselRef.value
if (!container) return
const slide_width = container.offsetWidth
container.scrollTo({
left: index * slide_width,
behavior: 'smooth'
})
current_slide.value = index
}
/**
* @description 启动自动轮播
* @returns {void}
*/
const start_auto_carousel = () => {
if (carousel_interval) clearInterval(carousel_interval)
if (!courses.value.length) return
carousel_interval = setInterval(() => {
if (!carouselRef.value) return
const total = courses.value.slice(0, 4).length
if (!total) return
const next = (current_slide.value + 1) % total
scroll_to_slide(next)
}, 5000)
}
onMounted(async () => {
await fetch_courses()
start_auto_carousel()
if (carouselRef.value) {
carouselRef.value.addEventListener('scroll', handle_carousel_scroll, { passive: true })
}
})
onUnmounted(() => {
if (carousel_interval) clearInterval(carousel_interval)
if (carouselRef.value) {
carouselRef.value.removeEventListener('scroll', handle_carousel_scroll)
}
})
</script>