hookehuyr

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

实现从课程详情页返回时恢复滚动位置的功能
添加数据属性标记课程目录项
新增滚动状态存储和恢复相关方法
......@@ -56,7 +56,7 @@
<div class="text-gray-700 text-sm leading-relaxed" v-html="course?.learning_goal"></div>
<br />
</div>
<van-empty v-if="!course?.feature && !course?.highlights && course?.learning_goal" description="暂无详情" />
<van-empty v-else description="暂无详情" />
</div>
<div class="h-2 bg-gray-100"></div>
......@@ -64,6 +64,7 @@
<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"
:data-lesson-id="lesson.id"
@click="goToStudyDetail(lesson.id)"
class="bg-white p-4 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-200 relative">
<div class="text-black text-base font-medium mb-2">{{ lesson.title }}</div>
......@@ -257,6 +258,94 @@ const course_type_maps = ref({
const task_list = ref([]);
const timeout_task_list = ref([]);
/**
* 生成课程滚动状态存储键
* @returns {string} 滚动状态存储键
* 注释:用于在会话存储中保存和恢复课程目录滚动位置
*/
const get_scroll_store_key = () => {
const course_id = router.currentRoute.value.params.id || '';
return `study_course_scroll_${course_id}`;
};
/**
* 判断是否需要恢复滚动位置
* @returns {boolean} 是否需要恢复滚动
* 注释:根据历史状态判断是否从课程详情页返回
*/
const should_restore_scroll = () => {
const forward = window.history && window.history.state ? window.history.state.forward : null;
if (!forward) return false;
return String(forward).includes('studyDetail');
};
/**
* 保存当前滚动位置
* @param {string} lesson_id - 当前课程目录项ID
* 注释:在用户滚动到目录项时调用,记录滚动位置和目录项ID
*/
const save_scroll_state = (lesson_id) => {
const key = get_scroll_store_key();
const payload = {
scroll_y: window.scrollY || 0,
lesson_id: lesson_id || '',
saved_at: Date.now(),
};
sessionStorage.setItem(key, JSON.stringify(payload));
};
/**
* 滚动到指定目录项
* @param {string} lesson_id - 目标目录项ID
* @returns {boolean} 是否成功滚动
* 注释:根据目录项ID找到元素并滚动到其位置,考虑顶部导航栏高度
*/
const scroll_to_lesson = (lesson_id) => {
if (!lesson_id) return false;
const el = document.querySelector(`[data-lesson-id="${lesson_id}"]`);
if (!el) return false;
const rect = el.getBoundingClientRect();
const buffer = 10;
const offset_top = (tabs_wrapper_height.value || 0) + buffer;
const top_offset = Math.max(0, rect.top + window.scrollY - offset_top);
window.scrollTo({ top: top_offset, left: 0, behavior: 'auto' });
return true;
};
/**
* 恢复滚动位置
* @returns {void}
* 注释:在组件挂载时调用,根据存储状态滚动到上次记录位置
*/
const restore_scroll_state = async () => {
if (!should_restore_scroll()) return;
const key = get_scroll_store_key();
const raw = sessionStorage.getItem(key);
if (!raw) return;
let state = null;
try {
state = JSON.parse(raw);
} catch (e) {
sessionStorage.removeItem(key);
return;
}
await nextTick();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const restored = scroll_to_lesson(state.lesson_id);
if (!restored && typeof state.scroll_y === 'number') {
window.scrollTo({ top: state.scroll_y, left: 0, behavior: 'auto' });
}
sessionStorage.removeItem(key);
};
onMounted(async () => {
/**
* 组件挂载时获取课程详情
......@@ -292,6 +381,8 @@ onMounted(async () => {
// 初始化标签容器宽度监听
initTabsContainerWidth();
handleScroll();
// 恢复滚动位置
restore_scroll_state();
});
// 在组件卸载时移除监听器
......@@ -393,6 +484,7 @@ const handleTabChange = (name) => {
};
const goToStudyDetail = (lessonId) => {
save_scroll_state(lessonId);
router.push(`/studyDetail/${lessonId}`);
};
......