hookehuyr

feat(课程详情页): 添加动态 favicon 功能

在课程详情页动态设置 favicon,使用课程封面作为图标。进入页面时插入 favicon 链接,离开时移除。针对 cdn.ipadbiz.cn 域名自动追加图片压缩参数优化加载性能。
...@@ -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)) {
......