FeaturedCoursesSection.vue 6.69 KB
<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>