StudyCoursePage.vue
11.9 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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
<!--
* @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>
<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>