hookehuyr

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

在课程详情页动态设置 favicon,使用课程封面作为图标。进入页面时插入 favicon 链接,离开时移除。针对 cdn.ipadbiz.cn 域名自动追加图片压缩参数优化加载性能。
......@@ -24,3 +24,9 @@ https://oa-dev.onwall.cn/f/mlaj
- 路由守卫:移除自动微信授权检查,新增 `startWxAuth` 供手动触发。
- 登录页:微信图标绑定点击事件,非微信环境提示“请在微信内打开”。
- 使用方式:进入登录页,点击微信图标进行授权登录。
- 课程详情页动态 favicon
- 行为:进入课程详情页时,在 `index.html``<head>` 中插入 `<link rel="icon" type="image/svg+xml" href="..." />`,图标为该课程 `cover`;离开页面时移除。
- CDN 规则:若图片域名为 `cdn.ipadbiz.cn`,自动追加 `?imageMogr2/thumbnail/200x/strip/quality/70`
- 位置:`/src/views/courses/CourseDetailPage.vue`,使用生命周期与 `watch` 监听封面变化来设置与清理。
- 函数:`build_favicon_url(src)``set_favicon(href)``remove_favicon()`
......
......@@ -332,7 +332,7 @@
</template>
<script setup lang="jsx">
import { ref, onMounted, defineComponent, h } from 'vue'
import { ref, onMounted, onUnmounted, defineComponent, h, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCart } from '@/contexts/cart'
import { useAuth } from '@/contexts/auth'
......@@ -349,9 +349,70 @@ import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "
import { addFavoriteAPI, cancelFavoriteAPI } from "@/api/favorite";
import { checkinTaskAPI } from '@/api/checkin';
/**
* @function build_favicon_url
* @description 构建 favicon 链接地址;若为 cdn.ipadbiz.cn 域名,则追加图片压缩参数。
* @param {string} src 原始图片地址
* @returns {string} 处理后的图片地址
*/
function build_favicon_url(src) {
// 若无地址,直接返回空字符串
if (!src) return '';
// 若为指定 CDN 域名,追加压缩参数(遵循项目图片规则)
if (src.includes('cdn.ipadbiz.cn')) {
const compress_param = 'imageMogr2/thumbnail/200x/strip/quality/70';
if (src.includes('?')) {
if (!src.includes(compress_param)) {
return src + '&' + compress_param;
}
} else {
return src + '?' + compress_param;
}
}
return src;
}
/**
* @function set_favicon
* @description 在页面 head 内动态插入或更新 favicon 链接标签。
* @param {string} href 图片地址(course 封面)
*/
function set_favicon(href) {
const head = document.head || document.getElementsByTagName('head')[0];
if (!head) return;
const icon_id = 'course-favicon';
const normalized = build_favicon_url(href);
// 若无可用地址,移除已存在的图标
if (!normalized) {
remove_favicon();
return;
}
let link = document.querySelector('link#' + icon_id);
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'icon');
// 按用户要求使用 image/svg+xml 类型
link.setAttribute('type', 'image/svg+xml');
link.setAttribute('id', icon_id);
head.appendChild(link);
}
link.setAttribute('href', normalized);
}
/**
* @function remove_favicon
* @description 从页面 head 中移除动态插入的课程 favicon。
*/
function remove_favicon() {
const icon_id = 'course-favicon';
const link = document.querySelector('link#' + icon_id);
if (link && link.parentNode) {
link.parentNode.removeChild(link);
}
}
const $route = useRoute();
const $router = useRouter();
useTitle(`${course.value.title || '课程详情'}`);
const route = useRoute()
const router = useRouter()
......@@ -397,6 +458,43 @@ const checkInSuccess = ref(false)
const { addToCart, proceedToCheckout } = useCart()
//
// 监听课程封面,进入详情页后动态设置 favicon,离开页面时移除
//
/**
* @function init_course_favicon_watch
* @description 初始化对 course 封面变化的监听,封面可用时设置 favicon。
*/
const init_course_favicon_watch = () => {
// 监听封面地址变化,立即触发以覆盖初次进入场景
const stop = watch(
() => (course.value && course.value.cover) || '',
(new_cover) => {
if (new_cover) {
// 封面可用时设置 favicon
set_favicon(new_cover);
}
},
{ immediate: true }
);
return stop;
};
// 进入页面时绑定监听;离开时清理并移除 favicon
let stop_watch_cover = null;
onMounted(() => {
stop_watch_cover = init_course_favicon_watch();
});
onUnmounted(() => {
if (typeof stop_watch_cover === 'function') {
stop_watch_cover();
stop_watch_cover = null;
}
// 离开详情页移除 favicon
remove_favicon();
});
// Handle favorite toggle
// 收藏/取消收藏操作
const toggleFavorite = async () => {
......@@ -547,6 +645,8 @@ onMounted(async () => {
isPurchased.value = foundCourse.is_buy;
isReviewed.value = foundCourse.is_comment;
useTitle(`${course.value.title || '课程详情'}`);
// 设置默认选中的 tab,确保选中的 tab 有内容
const availableTabs = curriculumItems.value;
if (availableTabs.length > 0 && !availableTabs.some(item => item.title === activeTab.value)) {
......