hookehuyr

feat: 为CDN图片添加压缩参数以优化加载性能

- 新增buildCdnImageUrl工具函数,为cdn域名图片自动追加七牛压缩参数
- 在多个组件中应用图片压缩,包括首页、课程卡片、活动卡片等
- 更新默认头像URL,添加压缩参数减少传输体积
- 更新README中的构建体积说明,反映当前优化状态
......@@ -53,7 +53,7 @@ src/
- 文档与代码存在不一致:README/CLAUDE 中的组件目录、路径描述与实际目录不一致
- 依赖与工程规范不够统一:同时存在 pnpm/yarn/npm 的 lock 文件,容易引起依赖漂移
- 构建产物体积偏大:存在 500kB 以上的 chunk(需要按路由/组件进一步拆分)
- 构建产物体积偏大:首屏 main chunk 已压到 500kB 内;仍有 video.js / pdf.js 等功能性依赖 chunk 超 500kB,但仅在对应功能触发时按需加载
- 状态来源较多:localStorage + contexts + axios 默认头并存,一致性风险偏高
- 全局屏蔽 warnHandler:[/src/main.js](file:///Users/huyirui/program/itomix/git/mlaj/src/main.js) 会吞掉 Vue 警告,可能掩盖潜在问题
- 目录存在历史遗留:[/src/layouts](file:///Users/huyirui/program/itomix/git/mlaj/src/layouts)[/src/components/layout](file:///Users/huyirui/program/itomix/git/mlaj/src/components/layout) 并存,建议后续清理归一
......
......@@ -11,7 +11,7 @@
<!-- Activity Image -->
<div class="w-1/3 h-28 relative">
<img
:src="activity.imageUrl"
:src="buildCdnImageUrl(activity.imageUrl)"
:alt="activity.title"
class="w-full h-full object-cover"
/>
......@@ -76,6 +76,7 @@
<script setup>
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import { buildCdnImageUrl } from '@/utils/tools'
const props = defineProps({
activity: {
......
......@@ -9,7 +9,7 @@
<router-link :to="linkTo || `/courses/${course.id}`" class="flex bg-white rounded-lg overflow-hidden shadow-sm">
<div class="w-1/3 h-28 relative">
<img
:src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
:src="buildCdnImageUrl(course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png')"
:alt="course.title"
class="w-full h-full object-cover"
/>
......@@ -44,6 +44,8 @@
</template>
<script setup>
import { buildCdnImageUrl } from '@/utils/tools'
/**
* @description 课程卡片组件
* @property {Object} course 课程对象
......
......@@ -15,7 +15,7 @@
<router-link :to="`/courses/${stream.id}`" class="block rounded-lg overflow-hidden shadow-sm relative">
<img
:src="stream.imageUrl"
:src="buildCdnImageUrl(stream.imageUrl)"
:alt="stream.title"
class="w-full h-28 object-cover"
/>
......@@ -43,6 +43,8 @@
</template>
<script setup>
import { buildCdnImageUrl } from '@/utils/tools'
defineProps({
/**
* 直播流数据对象
......
......@@ -16,7 +16,7 @@
>
<div class="relative rounded-xl overflow-hidden shadow-lg h-48">
<img
:src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
:src="buildCdnImageUrl(course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png', 400)"
:alt="course.title"
class="w-full h-full object-cover"
/>
......@@ -79,6 +79,7 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getCourseListAPI } from '@/api/course'
import { buildCdnImageUrl } from '@/utils/tools'
const courses = ref([])
const carouselRef = ref(null)
......
......@@ -117,7 +117,9 @@ const fetch_external_activities = async () => {
const mapped = list.map((item) => {
const xs_price = number_or_null(item?.xs_price)
const sc_price = number_or_null(item?.sc_price)
const imageUrl = item?.sl_img || item?.fx_img || item?.banner_img || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'
let imageUrl = item?.sl_img || item?.fx_img || item?.banner_img || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'
// 压缩阿里云图片资源
imageUrl = imageUrl+ '?x-oss-process=image/quality,q_70/resize,w_100'
const period = format_period(item?.act_start_at, item?.act_end_at)
const upper = number_or_null(item?.stu_num_upper)
......
......@@ -21,7 +21,7 @@
<div class="flex flex-col h-full" @click="go_to_course_detail(item)">
<div class="h-28 mb-2 rounded-lg overflow-hidden relative">
<img
:src="item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
:src="buildCdnImageUrl(item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png')"
:alt="item.title"
class="w-full h-full object-cover"
/>
......@@ -45,6 +45,7 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import { getCourseListAPI } from '@/api/course'
import { buildCdnImageUrl } from '@/utils/tools'
const router = useRouter()
......
......@@ -15,7 +15,7 @@ export function useImageLoader() {
/**
* 默认头像 URL
*/
const DEFAULT_AVATAR = 'https://cdn.ipadbiz.cn/mlaj/images/default-avatar.jpeg'
const DEFAULT_AVATAR = 'https://cdn.ipadbiz.cn/mlaj/images/default-avatar.jpeg?imageMogr2/thumbnail/200x/strip/quality/70'
/**
* 处理图片加载错误
......
/*
* @Date: 2022-04-18 15:59:42
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-21 13:33:36
* @LastEditTime: 2026-01-24 14:35:27
* @FilePath: /mlaj/src/utils/tools.js
* @Description: 文件描述
*/
......@@ -127,6 +127,35 @@ const formatDuration = (seconds) => {
};
/**
* @description 为 CDN 图片追加七牛压缩参数,降低首页等场景的图片体积。
* @param {string} src 原始图片地址
* @param {number} [width=200] 缩略图宽度(像素)
* @param {number} [quality=70] 图片质量(1-100)
* @returns {string} 处理后的图片地址
*/
const buildCdnImageUrl = (src, width = 200, quality = 70) => {
const url = typeof src === 'string' ? src : '';
if (!url) return '';
// 已包含七牛处理参数时不重复追加,避免不确定行为
if (url.includes('imageMogr2')) return url;
try {
const u = new URL(url, window.location.origin);
// 兼容多个 CDN 域名:只要域名前缀以 cdn 开头,就允许追加七牛参数
// 例如:cdn.ipadbiz.cn / cdn.xxx.com / cdn1.xxx.com
if (!/^cdn/i.test(u.hostname)) return url;
const [base, hash] = url.split('#');
const param = `imageMogr2/thumbnail/${width}x/strip/quality/${quality}`;
const next = base + (base.includes('?') ? '&' : '?') + param;
return hash ? `${next}#${hash}` : next;
} catch (e) {
return url;
}
};
/**
* @description 归一化“打卡任务列表”字段,避免各页面散落 map 导致漏改。
* @param {Array<any>} list 原始任务列表(通常为接口返回 task_list/timeout_task_list)
* @returns {Array<{id: number|string, name: string, task_type: string, is_gray: boolean, is_finish: boolean, checkin_subtask_id: number|string|undefined}>}
......@@ -289,6 +318,7 @@ export {
getUrlParams,
stringifyQuery,
formatDuration,
buildCdnImageUrl,
normalizeCheckinTaskItems,
normalizeAttachmentTypeConfig,
};
......
......@@ -38,7 +38,7 @@
<div class="flex items-center">
<div class="w-10 h-10 rounded-full overflow-hidden mr-3">
<img
:src="currentUser?.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
:src="buildCdnImageUrl(currentUser?.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')"
class="w-full h-full object-cover"
@error="handleImageError" />
</div>
......@@ -231,7 +231,7 @@
<div class="flex items-center">
<div class="w-12 h-12 bg-green-100 rounded-lg overflow-hidden mr-3 flex-shrink-0">
<img
:src="item.image"
:src="buildCdnImageUrl(item.image)"
:alt="item.title"
class="w-full h-full object-cover"
@error="handleImageError"
......@@ -287,7 +287,7 @@
>
<template v-if="activeVideoIndex !== index">
<img
:src="item.image"
:src="buildCdnImageUrl(item.image, 400)"
:alt="item.title"
class="w-full h-full object-cover"
@error="handleImageError"
......@@ -350,7 +350,7 @@ import { useImageLoader } from '@/composables/useImageLoader'
// 导入接口
import { getTaskListAPI } from "@/api/checkin";
import { normalizeCheckinTaskItems } from '@/utils/tools'
import { normalizeCheckinTaskItems, buildCdnImageUrl } from '@/utils/tools'
// 图片加载错误处理
const { handleImageError } = useImageLoader()
......