feat(课程页面): 添加课程目录滚动位置记忆功能
实现从课程详情页返回时恢复滚动位置的功能 添加数据属性标记课程目录项 新增滚动状态存储和恢复相关方法
Showing
1 changed file
with
93 additions
and
1 deletions
| ... | @@ -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 | ... | ... |
-
Please register or login to post a comment