StudyCoursePage.vue 11.9 KB
<!--
 * @Date: 2024-01-17
 * @Description: 课程详情页面
-->
<template>
    <div class="study-course-page bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen">
        <div v-if="course" class="flex flex-col h-screen">
            <!-- 固定区域:课程封面和标签页 -->
            <div class=" bg-white">
                <!-- 课程封面区域 -->
                <van-image class="w-full aspect-video object-cover" :src="course?.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'" :alt="course?.title" />
                <div class="p-4">
                    <h1 class="text-black text-xl font-bold mb-2">{{ course?.title }}</h1>
                    <div class="flex items-center text-gray-500 text-sm">
                        <span>已更新 没有字段 期</span>
                        <span class="mx-2">|</span>
                        <span>没有字段 人订阅</span>
                    </div>
                </div>

                <div class="h-2 bg-gray-100"></div>

                <!-- 标签页区域 -->
                <div class="py-3 bg-white transition-all duration-300" :class="{'fixed top-0 left-0 right-0 z-10': isTabFixed}" :style="isTabFixed ? { transform: `translateY(0)` } : {}">
                    <div class="flex justify-around items-center relative">
                        <div
                            v-for="(tab, index) in [{title: '详情', name: 'detail'}, {title: '目录', name: 'catalog'}, {title: '课程互动', name: 'interaction'}]"
                            :key="tab.name"
                            :class="['px-4 py-2 cursor-pointer transition-colors relative', activeTab === tab.name ? 'text-green-600 font-medium' : 'text-gray-600']"
                            @click="handleTabChange(tab.name)"
                            ref="tabRefs"
                        >
                            {{ tab.title }}
                        </div>
                        <div
                            class="absolute bottom-0 left-0 bg-green-600 transition-all duration-300 z-20"
                            :style="{
                                width: tabRefs && tabRefs[activeTab === 'detail' ? 0 : activeTab === 'catalog' ? 1 : 2]?.offsetWidth + 'px',
                                transform: `translateX(${tabRefs && tabRefs[activeTab === 'detail' ? 0 : activeTab === 'catalog' ? 1 : 2]?.offsetLeft}px)`,
                                height: '2px',
                            }"
                        ></div>
                    </div>
                </div>
            </div>

            <!-- 滚动区域:详情、目录和互动内容 -->
            <div class="overflow-y-auto flex-1"
                :style="{ paddingTop: topWrapperHeight, height: 'calc(100vh - ' + topWrapperHeight + ')' }">
                <!-- 详情区域 -->
                <div id="detail" class="py-4 px-4">
                    <div v-if="course?.feature">
                        <div class="text-black text-xl font-bold mb-2">课程特色</div>
                        <div class="text-gray-700 text-sm leading-relaxed" v-html="course?.feature"></div>
                        <br />
                    </div>
                    <div v-if="course?.highlights">
                        <div class="text-black text-xl font-bold mb-2">课程亮点</div>
                        <div class="text-gray-700 text-sm leading-relaxed" v-html="course?.highlights"></div>
                        <br />
                    </div>
                    <div v-if="course?.learning_goal">
                        <div class="text-black text-xl font-bold mb-2">学习目标</div>
                        <div class="text-gray-700 text-sm leading-relaxed" v-html="course?.learning_goal"></div>
                        <br />
                    </div>
                    <van-empty v-else description="暂无详情" />
                </div>

                <div class="h-2 bg-gray-100"></div>

                <!-- 目录区域 -->
                <div id="catalog" class="py-4">
                    <div v-if="course_lessons.length" class="space-y-4">
                        <div v-for="(lesson, index) in course_lessons" :key="index"
                            @click="router.push(`/studyDetail/${lesson.id}`)"
                            class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
                            <div v-if="lesson.progress > 0 && lesson.progress < 100"
                                class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded">
                                没有字段上次看到</div>
                            <div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div>
                            <div class="flex items-center text-sm text-gray-500">
                                <span>{{ course_type_maps[lesson.course_type] }}</span>&nbsp;
                                <span>{{ dayjs(course.schedule_time).format('YYYY-MM-DD') }}</span>
                                <span class="mx-2">|</span>
                                <span>没有字段 次学习</span>
                                <span class="mx-2">|</span>
                                <span>没有字段 已学习{{ lesson?.progress }}%</span>
                            </div>
                        </div>
                    </div>
                    <van-empty v-else description="暂无目录" />
                </div>

                <div class="h-2 bg-gray-100"></div>

                <!-- 互动区域 -->
                <div id="interaction" class="py-4 px-4">
                    <div class="bg-white rounded-lg p-4 mb-4 cursor-pointer">
                        <div class="flex items-center justify-between">
                            <div class="flex items-center gap-3">
                                <van-icon size="3rem" name="calendar-o" class="text-xl text-gray-600" />
                                <div>
                                    <div class="text-base font-medium">打卡</div>
                                    <div class="text-sm text-gray-500">关联7个打卡</div>
                                </div>
                            </div>
                            <van-icon name="arrow" class="text-gray-400" />
                        </div>
                    </div>
                </div>
                <!-- 添加底部填充区域 -->
                <div style="height: 30vh;"></div>
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref, onMounted, nextTick, onUnmounted } from 'vue';
import { useTitle } from '@vueuse/core';
import { useRouter } from "vue-router";
import dayjs from 'dayjs';

// 导入接口
import { getCourseDetailAPI } from '@/api/course'

const router = useRouter();

// 页面标题
useTitle('课程详情');

// 当前激活的标签页
const activeTab = ref('detail');
const topWrapperHeight = ref(0);
const resizeObserver = ref(null);
const tabRefs = ref([]);

// 计算topWrapperHeight的函数
const updateTopWrapperHeight = () => {
    nextTick(() => {
        const topWrapper = document.querySelector('.top-wrapper');
        if (topWrapper) {
            // 断开之前的observer连接
            if (resizeObserver.value) {
                resizeObserver.value.disconnect();
            }
            // 使用 ResizeObserver 监听元素尺寸变化
            resizeObserver.value = new ResizeObserver(() => {
                topWrapperHeight.value = `${topWrapper.offsetHeight}px`;
            });
            resizeObserver.value.observe(topWrapper);
        }
    });
};

const course = ref([]);
const course_lessons = ref([]);
const course_type_maps = ref({
    video: '视频',
    audio: '音频',
    image: '图片',
    file: '文件',
})

onMounted(async () => {
    /**
     * 组件挂载时获取课程详情
     */
    // 获取课程ID
    const courseId = router.currentRoute.value.params.id;
    // 调用接口获取课程详情
    const { code, data } = await getCourseDetailAPI({ i: courseId });
    if (code) {
        course.value = data;
        course_lessons.value = data.schedule || [];
    }
    /**
     * 初始化时计算topWrapperHeight
     */
    // 添加滚动监听
    window.addEventListener('scroll', handleScroll);
    // 添加窗口大小变化监听
    window.addEventListener('resize', updateTopWrapperHeight);
    // 确保在组件挂载后计算高度
    updateTopWrapperHeight();
});

// 在组件卸载时移除监听器
onUnmounted(() => {
    window.removeEventListener('scroll', handleScroll);
    window.removeEventListener('resize', updateTopWrapperHeight);
    // 组件卸载时取消监听
    if (resizeObserver.value) {
        resizeObserver.value.disconnect();
        resizeObserver.value = null;
    }
});

// 处理滚动事件
// 在script setup中添加
const isTabFixed = ref(false);
const tabOriginalTop = ref(0);

onMounted(async () => {
    setTimeout(() => {
        nextTick(() => {
            // 记录标签页原始位置
            const tabElement = document.querySelector('.py-3.bg-white');
            if (tabElement) {
                tabOriginalTop.value = tabElement.getBoundingClientRect().top + window.scrollY;
            }
        })
    }, 500);
});

// 修改handleScroll函数
const handleScroll = () => {
    const detailElement = document.getElementById('detail');
    const catalogElement = document.getElementById('catalog');
    const interactionElement = document.getElementById('interaction');
    const tabElement = document.querySelector('.py-3.bg-white');
    if (!detailElement || !catalogElement || !interactionElement || !tabElement) return;

    const currentScrollY = window.scrollY;
    isTabFixed.value = currentScrollY >= tabOriginalTop.value;

    const scrollTop = window.scrollY;
    const catalogOffset = catalogElement.offsetTop - (isTabFixed.value ? tabElement.offsetHeight : 0);
    const interactionOffset = interactionElement.offsetTop - (isTabFixed.value ? tabElement.offsetHeight : 0);

    if (scrollTop >= interactionOffset) {
        activeTab.value = 'interaction';
    } else if (scrollTop >= catalogOffset) {
        activeTab.value = 'catalog';
    } else {
        activeTab.value = 'detail';
    }
};

// 处理标签页切换
// 在script setup中添加
const previousTab = ref('detail');

const getTransitionTiming = (current, previous) => {
    const tabOrder = { detail: 0, catalog: 1, interaction: 2 };
    return tabOrder[current] > tabOrder[previous] ? 'cubic-bezier(0.4, 0, 0.2, 1)' : 'cubic-bezier(0.4, 0, 0.2, 1)';
};

// 修改handleTabChange函数
const handleTabChange = (name) => {
    previousTab.value = activeTab.value;
    nextTick(() => {
        const element = document.getElementById(name);
        if (element) {
            const topOffset = element.offsetTop - parseInt(topWrapperHeight.value);
            window.scrollTo({
                top: topOffset,
                behavior: 'smooth'
            });
        }
    });
};

// 课程数据
// const course = ref({
//     title: '开学礼·止的智慧·心法老师·20241001',
//     coverImage: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
//     updateTime: '2024.01.17',
//     viewCount: 1897,
//     description: '这是一门关于心法的课程,帮助学员掌握止的智慧...',
//     lessons: [
//         {
//             title: '第一课:止的基础',
//             duration: '45分钟',
//             progress: 100
//         },
//         {
//             title: '第二课:止的技巧',
//             duration: '50分钟',
//             progress: 60
//         },
//         {
//             title: '第三课:止的应用',
//             duration: '40分钟',
//             progress: 0
//         }
//     ]
// });
</script>

<style scoped>
.study-course-page {
    min-height: 100vh;
}

.course-header {
    height: auto;
}

.course-tabs {
    background-color: #fff;
}

.course-catalog {
    background-color: #fff;
}
</style>