hookehuyr

feat(课程页面): 添加课程目录滚动位置记忆功能

实现从课程详情页返回时恢复滚动位置的功能
添加数据属性标记课程目录项
新增滚动状态存储和恢复相关方法
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
56 <div class="text-gray-700 text-sm leading-relaxed" v-html="course?.learning_goal"></div> 56 <div class="text-gray-700 text-sm leading-relaxed" v-html="course?.learning_goal"></div>
57 <br /> 57 <br />
58 </div> 58 </div>
59 - <van-empty v-if="!course?.feature && !course?.highlights && course?.learning_goal" description="暂无详情" /> 59 + <van-empty v-else description="暂无详情" />
60 </div> 60 </div>
61 61
62 <div class="h-2 bg-gray-100"></div> 62 <div class="h-2 bg-gray-100"></div>
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
64 <div id="catalog" class="py-4"> 64 <div id="catalog" class="py-4">
65 <div v-if="course_lessons.length" class="space-y-4"> 65 <div v-if="course_lessons.length" class="space-y-4">
66 <div v-for="(lesson, index) in course_lessons" :key="index" 66 <div v-for="(lesson, index) in course_lessons" :key="index"
67 + :data-lesson-id="lesson.id"
67 @click="goToStudyDetail(lesson.id)" 68 @click="goToStudyDetail(lesson.id)"
68 class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative"> 69 class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
69 <div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div> 70 <div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div>
...@@ -257,6 +258,94 @@ const course_type_maps = ref({ ...@@ -257,6 +258,94 @@ const course_type_maps = ref({
257 const task_list = ref([]); 258 const task_list = ref([]);
258 const timeout_task_list = ref([]); 259 const timeout_task_list = ref([]);
259 260
261 +/**
262 + * 生成课程滚动状态存储键
263 + * @returns {string} 滚动状态存储键
264 + * 注释:用于在会话存储中保存和恢复课程目录滚动位置
265 + */
266 +
267 +const get_scroll_store_key = () => {
268 + const course_id = router.currentRoute.value.params.id || '';
269 + return `study_course_scroll_${course_id}`;
270 +};
271 +
272 +/**
273 + * 判断是否需要恢复滚动位置
274 + * @returns {boolean} 是否需要恢复滚动
275 + * 注释:根据历史状态判断是否从课程详情页返回
276 + */
277 +
278 +const should_restore_scroll = () => {
279 + const forward = window.history && window.history.state ? window.history.state.forward : null;
280 + if (!forward) return false;
281 + return String(forward).includes('studyDetail');
282 +};
283 +
284 +/**
285 + * 保存当前滚动位置
286 + * @param {string} lesson_id - 当前课程目录项ID
287 + * 注释:在用户滚动到目录项时调用,记录滚动位置和目录项ID
288 + */
289 +
290 +const save_scroll_state = (lesson_id) => {
291 + const key = get_scroll_store_key();
292 + const payload = {
293 + scroll_y: window.scrollY || 0,
294 + lesson_id: lesson_id || '',
295 + saved_at: Date.now(),
296 + };
297 + sessionStorage.setItem(key, JSON.stringify(payload));
298 +};
299 +
300 +/**
301 + * 滚动到指定目录项
302 + * @param {string} lesson_id - 目标目录项ID
303 + * @returns {boolean} 是否成功滚动
304 + * 注释:根据目录项ID找到元素并滚动到其位置,考虑顶部导航栏高度
305 + */
306 +
307 +const scroll_to_lesson = (lesson_id) => {
308 + if (!lesson_id) return false;
309 + const el = document.querySelector(`[data-lesson-id="${lesson_id}"]`);
310 + if (!el) return false;
311 + const rect = el.getBoundingClientRect();
312 + const buffer = 10;
313 + const offset_top = (tabs_wrapper_height.value || 0) + buffer;
314 + const top_offset = Math.max(0, rect.top + window.scrollY - offset_top);
315 + window.scrollTo({ top: top_offset, left: 0, behavior: 'auto' });
316 + return true;
317 +};
318 +
319 +/**
320 + * 恢复滚动位置
321 + * @returns {void}
322 + * 注释:在组件挂载时调用,根据存储状态滚动到上次记录位置
323 + */
324 +
325 +const restore_scroll_state = async () => {
326 + if (!should_restore_scroll()) return;
327 + const key = get_scroll_store_key();
328 + const raw = sessionStorage.getItem(key);
329 + if (!raw) return;
330 +
331 + let state = null;
332 + try {
333 + state = JSON.parse(raw);
334 + } catch (e) {
335 + sessionStorage.removeItem(key);
336 + return;
337 + }
338 +
339 + await nextTick();
340 + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
341 +
342 + const restored = scroll_to_lesson(state.lesson_id);
343 + if (!restored && typeof state.scroll_y === 'number') {
344 + window.scrollTo({ top: state.scroll_y, left: 0, behavior: 'auto' });
345 + }
346 + sessionStorage.removeItem(key);
347 +};
348 +
260 onMounted(async () => { 349 onMounted(async () => {
261 /** 350 /**
262 * 组件挂载时获取课程详情 351 * 组件挂载时获取课程详情
...@@ -292,6 +381,8 @@ onMounted(async () => { ...@@ -292,6 +381,8 @@ onMounted(async () => {
292 // 初始化标签容器宽度监听 381 // 初始化标签容器宽度监听
293 initTabsContainerWidth(); 382 initTabsContainerWidth();
294 handleScroll(); 383 handleScroll();
384 + // 恢复滚动位置
385 + restore_scroll_state();
295 }); 386 });
296 387
297 // 在组件卸载时移除监听器 388 // 在组件卸载时移除监听器
...@@ -393,6 +484,7 @@ const handleTabChange = (name) => { ...@@ -393,6 +484,7 @@ const handleTabChange = (name) => {
393 }; 484 };
394 485
395 const goToStudyDetail = (lessonId) => { 486 const goToStudyDetail = (lessonId) => {
487 + save_scroll_state(lessonId);
396 router.push(`/studyDetail/${lessonId}`); 488 router.push(`/studyDetail/${lessonId}`);
397 }; 489 };
398 490
......