hookehuyr

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

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