feat: 为CDN图片添加压缩参数以优化加载性能
- 新增buildCdnImageUrl工具函数,为cdn域名图片自动追加七牛压缩参数 - 在多个组件中应用图片压缩,包括首页、课程卡片、活动卡片等 - 更新默认头像URL,添加压缩参数减少传输体积 - 更新README中的构建体积说明,反映当前优化状态
Showing
10 changed files
with
52 additions
and
13 deletions
| ... | @@ -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() | ... | ... |
-
Please register or login to post a comment