feat(课程详情页): 添加动态 favicon 功能
在课程详情页动态设置 favicon,使用课程封面作为图标。进入页面时插入 favicon 链接,离开时移除。针对 cdn.ipadbiz.cn 域名自动追加图片压缩参数优化加载性能。
Showing
2 changed files
with
108 additions
and
2 deletions
| ... | @@ -24,3 +24,9 @@ https://oa-dev.onwall.cn/f/mlaj | ... | @@ -24,3 +24,9 @@ https://oa-dev.onwall.cn/f/mlaj |
| 24 | - 路由守卫:移除自动微信授权检查,新增 `startWxAuth` 供手动触发。 | 24 | - 路由守卫:移除自动微信授权检查,新增 `startWxAuth` 供手动触发。 |
| 25 | - 登录页:微信图标绑定点击事件,非微信环境提示“请在微信内打开”。 | 25 | - 登录页:微信图标绑定点击事件,非微信环境提示“请在微信内打开”。 |
| 26 | - 使用方式:进入登录页,点击微信图标进行授权登录。 | 26 | - 使用方式:进入登录页,点击微信图标进行授权登录。 |
| 27 | + | ||
| 28 | + - 课程详情页动态 favicon | ||
| 29 | + - 行为:进入课程详情页时,在 `index.html` 的 `<head>` 中插入 `<link rel="icon" type="image/svg+xml" href="..." />`,图标为该课程 `cover`;离开页面时移除。 | ||
| 30 | + - CDN 规则:若图片域名为 `cdn.ipadbiz.cn`,自动追加 `?imageMogr2/thumbnail/200x/strip/quality/70`。 | ||
| 31 | + - 位置:`/src/views/courses/CourseDetailPage.vue`,使用生命周期与 `watch` 监听封面变化来设置与清理。 | ||
| 32 | + - 函数:`build_favicon_url(src)`、`set_favicon(href)`、`remove_favicon()`。 | ... | ... |
| ... | @@ -332,7 +332,7 @@ | ... | @@ -332,7 +332,7 @@ |
| 332 | </template> | 332 | </template> |
| 333 | 333 | ||
| 334 | <script setup lang="jsx"> | 334 | <script setup lang="jsx"> |
| 335 | -import { ref, onMounted, defineComponent, h } from 'vue' | 335 | +import { ref, onMounted, onUnmounted, defineComponent, h, watch } from 'vue' |
| 336 | import { useRoute, useRouter } from 'vue-router' | 336 | import { useRoute, useRouter } from 'vue-router' |
| 337 | import { useCart } from '@/contexts/cart' | 337 | import { useCart } from '@/contexts/cart' |
| 338 | import { useAuth } from '@/contexts/auth' | 338 | import { useAuth } from '@/contexts/auth' |
| ... | @@ -349,9 +349,70 @@ import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from " | ... | @@ -349,9 +349,70 @@ import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from " |
| 349 | import { addFavoriteAPI, cancelFavoriteAPI } from "@/api/favorite"; | 349 | import { addFavoriteAPI, cancelFavoriteAPI } from "@/api/favorite"; |
| 350 | import { checkinTaskAPI } from '@/api/checkin'; | 350 | import { checkinTaskAPI } from '@/api/checkin'; |
| 351 | 351 | ||
| 352 | +/** | ||
| 353 | + * @function build_favicon_url | ||
| 354 | + * @description 构建 favicon 链接地址;若为 cdn.ipadbiz.cn 域名,则追加图片压缩参数。 | ||
| 355 | + * @param {string} src 原始图片地址 | ||
| 356 | + * @returns {string} 处理后的图片地址 | ||
| 357 | + */ | ||
| 358 | +function build_favicon_url(src) { | ||
| 359 | + // 若无地址,直接返回空字符串 | ||
| 360 | + if (!src) return ''; | ||
| 361 | + // 若为指定 CDN 域名,追加压缩参数(遵循项目图片规则) | ||
| 362 | + if (src.includes('cdn.ipadbiz.cn')) { | ||
| 363 | + const compress_param = 'imageMogr2/thumbnail/200x/strip/quality/70'; | ||
| 364 | + if (src.includes('?')) { | ||
| 365 | + if (!src.includes(compress_param)) { | ||
| 366 | + return src + '&' + compress_param; | ||
| 367 | + } | ||
| 368 | + } else { | ||
| 369 | + return src + '?' + compress_param; | ||
| 370 | + } | ||
| 371 | + } | ||
| 372 | + return src; | ||
| 373 | +} | ||
| 374 | + | ||
| 375 | +/** | ||
| 376 | + * @function set_favicon | ||
| 377 | + * @description 在页面 head 内动态插入或更新 favicon 链接标签。 | ||
| 378 | + * @param {string} href 图片地址(course 封面) | ||
| 379 | + */ | ||
| 380 | +function set_favicon(href) { | ||
| 381 | + const head = document.head || document.getElementsByTagName('head')[0]; | ||
| 382 | + if (!head) return; | ||
| 383 | + const icon_id = 'course-favicon'; | ||
| 384 | + const normalized = build_favicon_url(href); | ||
| 385 | + // 若无可用地址,移除已存在的图标 | ||
| 386 | + if (!normalized) { | ||
| 387 | + remove_favicon(); | ||
| 388 | + return; | ||
| 389 | + } | ||
| 390 | + let link = document.querySelector('link#' + icon_id); | ||
| 391 | + if (!link) { | ||
| 392 | + link = document.createElement('link'); | ||
| 393 | + link.setAttribute('rel', 'icon'); | ||
| 394 | + // 按用户要求使用 image/svg+xml 类型 | ||
| 395 | + link.setAttribute('type', 'image/svg+xml'); | ||
| 396 | + link.setAttribute('id', icon_id); | ||
| 397 | + head.appendChild(link); | ||
| 398 | + } | ||
| 399 | + link.setAttribute('href', normalized); | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +/** | ||
| 403 | + * @function remove_favicon | ||
| 404 | + * @description 从页面 head 中移除动态插入的课程 favicon。 | ||
| 405 | + */ | ||
| 406 | +function remove_favicon() { | ||
| 407 | + const icon_id = 'course-favicon'; | ||
| 408 | + const link = document.querySelector('link#' + icon_id); | ||
| 409 | + if (link && link.parentNode) { | ||
| 410 | + link.parentNode.removeChild(link); | ||
| 411 | + } | ||
| 412 | +} | ||
| 413 | + | ||
| 352 | const $route = useRoute(); | 414 | const $route = useRoute(); |
| 353 | const $router = useRouter(); | 415 | const $router = useRouter(); |
| 354 | -useTitle(`${course.value.title || '课程详情'}`); | ||
| 355 | 416 | ||
| 356 | const route = useRoute() | 417 | const route = useRoute() |
| 357 | const router = useRouter() | 418 | const router = useRouter() |
| ... | @@ -397,6 +458,43 @@ const checkInSuccess = ref(false) | ... | @@ -397,6 +458,43 @@ const checkInSuccess = ref(false) |
| 397 | 458 | ||
| 398 | const { addToCart, proceedToCheckout } = useCart() | 459 | const { addToCart, proceedToCheckout } = useCart() |
| 399 | 460 | ||
| 461 | +// | ||
| 462 | +// 监听课程封面,进入详情页后动态设置 favicon,离开页面时移除 | ||
| 463 | +// | ||
| 464 | +/** | ||
| 465 | + * @function init_course_favicon_watch | ||
| 466 | + * @description 初始化对 course 封面变化的监听,封面可用时设置 favicon。 | ||
| 467 | + */ | ||
| 468 | +const init_course_favicon_watch = () => { | ||
| 469 | + // 监听封面地址变化,立即触发以覆盖初次进入场景 | ||
| 470 | + const stop = watch( | ||
| 471 | + () => (course.value && course.value.cover) || '', | ||
| 472 | + (new_cover) => { | ||
| 473 | + if (new_cover) { | ||
| 474 | + // 封面可用时设置 favicon | ||
| 475 | + set_favicon(new_cover); | ||
| 476 | + } | ||
| 477 | + }, | ||
| 478 | + { immediate: true } | ||
| 479 | + ); | ||
| 480 | + return stop; | ||
| 481 | +}; | ||
| 482 | + | ||
| 483 | +// 进入页面时绑定监听;离开时清理并移除 favicon | ||
| 484 | +let stop_watch_cover = null; | ||
| 485 | +onMounted(() => { | ||
| 486 | + stop_watch_cover = init_course_favicon_watch(); | ||
| 487 | +}); | ||
| 488 | + | ||
| 489 | +onUnmounted(() => { | ||
| 490 | + if (typeof stop_watch_cover === 'function') { | ||
| 491 | + stop_watch_cover(); | ||
| 492 | + stop_watch_cover = null; | ||
| 493 | + } | ||
| 494 | + // 离开详情页移除 favicon | ||
| 495 | + remove_favicon(); | ||
| 496 | +}); | ||
| 497 | + | ||
| 400 | // Handle favorite toggle | 498 | // Handle favorite toggle |
| 401 | // 收藏/取消收藏操作 | 499 | // 收藏/取消收藏操作 |
| 402 | const toggleFavorite = async () => { | 500 | const toggleFavorite = async () => { |
| ... | @@ -547,6 +645,8 @@ onMounted(async () => { | ... | @@ -547,6 +645,8 @@ onMounted(async () => { |
| 547 | isPurchased.value = foundCourse.is_buy; | 645 | isPurchased.value = foundCourse.is_buy; |
| 548 | isReviewed.value = foundCourse.is_comment; | 646 | isReviewed.value = foundCourse.is_comment; |
| 549 | 647 | ||
| 648 | + useTitle(`${course.value.title || '课程详情'}`); | ||
| 649 | + | ||
| 550 | // 设置默认选中的 tab,确保选中的 tab 有内容 | 650 | // 设置默认选中的 tab,确保选中的 tab 有内容 |
| 551 | const availableTabs = curriculumItems.value; | 651 | const availableTabs = curriculumItems.value; |
| 552 | if (availableTabs.length > 0 && !availableTabs.some(item => item.title === activeTab.value)) { | 652 | if (availableTabs.length > 0 && !availableTabs.some(item => item.title === activeTab.value)) { | ... | ... |
-
Please register or login to post a comment