diff --git a/vite-tailwindcss/index.html b/vite-tailwindcss/index.html index 5b385d0..9e50784 100644 --- a/vite-tailwindcss/index.html +++ b/vite-tailwindcss/index.html @@ -11,6 +11,8 @@ + + 💕李琦❤️李逸烁 💒婚礼请柬 diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 58d3f2a..1503f92 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -238,6 +238,34 @@ const preloadNineGridImages = () => { } } +/** 安卓专项:把当前 page 的所有图都预热(不创建 DOM,仅触发浏览器缓存下载) + * 让浏览器在空闲时下载+解码,避免滑到时才下载造成卡顿。 + * iOS 不预热,保留原串行体验。 */ +const isAndroidClient = (() => { + if (typeof navigator === 'undefined') return false + const ua = navigator.userAgent || '' + return /Android/i.test(ua) && !/CrOS/i.test(ua) +})() +let androidPreloadStarted = false +const preloadAllForAndroid = () => { + if (!isAndroidClient || androidPreloadStarted) return + const urls = [ + props.page?.img1, + props.page?.img2, + ...(props.page?.images || []), + ].filter(Boolean) + if (!urls.length) return + androidPreloadStarted = true + // 错开下载,避免同时抢占首屏网络 + urls.forEach((url, i) => { + setTimeout(() => { + const img = new Image() + img.decoding = 'async' + img.src = url + }, 800 + i * 120) + }) +} + const runNineSequential = async () => { const total = (props.page.images || []).length const { cancelled } = await seq.run({ @@ -378,6 +406,7 @@ watch( onMounted(() => { if (!props.nineGridAnimate) preloadNineGridImages() + preloadAllForAndroid() if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver() }) diff --git a/vite-tailwindcss/src/composables/useAndroidOptimize.js b/vite-tailwindcss/src/composables/useAndroidOptimize.js new file mode 100644 index 0000000..c8e9033 --- /dev/null +++ b/vite-tailwindcss/src/composables/useAndroidOptimize.js @@ -0,0 +1,71 @@ +/** + * 安卓专项优化(iOS 保持原逻辑) + * + * - 检测是否为安卓 + * - 提供图片 URL 改写工具:把阿里云 OSS 图压缩为 750px 宽 + webp + * 仅对安卓生效,iOS 原样返回,确保 iOS 视觉体验不变 + */ + +const isAndroidClient = (() => { + if (typeof navigator === 'undefined') return false + const ua = navigator.userAgent || '' + // 排除 ChromeOS / Android 的 CrOS 伪装;保留真正的 Android 移动端 + return /Android/i.test(ua) && !/CrOS/i.test(ua) +})() + +export const isAndroid = isAndroidClient + +const OSS_HOST = 'aliyuncs.com' +const PROC_750 = 'image/resize,w_750,lfit/format,webp/q,85' + +/** + * 判断 URL 是否为阿里云 OSS 图(仅处理 OSS 图,不影响其它域/本地/blob URL) + */ +const isOssImage = (url) => { + if (typeof url !== 'string') return false + const u = url.trim() + if (!u || u.startsWith('data:') || u.startsWith('blob:')) return false + // 仅 http/https + if (!/^https?:\/\//i.test(u)) return false + // 排除图标/iconify 等非图片 + if (/iconify\.design|\.svg(\?|$)/i.test(u)) return false + return u.includes(OSS_HOST) +} + +/** + * 对单张 OSS 图片 URL 追加 OSS 处理参数(仅安卓) + * - 已存在 x-oss-process 时合并/覆盖 resize 与 format + * - 不改变非 OSS URL + * + * 注意:默认禁用(如下一行 return)。如需启用,注释掉该行。 + * 之前出现"图片加载不出来",是因为部分 OSS 桶未开启图片处理服务 / + * 或 URL 已是 webp,再叠加 format,webp 在某些 bucket 配置下返回 400。 + * 启用前请确认你的 OSS 已开通 "图片处理" 服务且支持 x-oss-process。 + */ +export const optimizeImageUrl = (url) => { + return url + /* eslint-disable no-unreachable */ + if (!isAndroidClient) return url + if (!isOssImage(url)) return url + const u = url.trim() + // 已有 x-oss-process:替换其中 resize / format + const qIdx = u.indexOf('?x-oss-process=') + if (qIdx >= 0) { + const before = u.slice(0, qIdx) + let proc = u.slice(qIdx + '?x-oss-process='.length) + proc = proc + .split('/') + .filter((seg) => !seg.startsWith('resize') && !seg.startsWith('format') && !seg.startsWith('q')) + .join('/') + const merged = proc ? `${PROC_750}/${proc}` : PROC_750 + return `${before}?x-oss-process=${merged}` + } + return `${u}?x-oss-process=${PROC_750}` + /* eslint-enable no-unreachable */ +} + +/** + * 批量优化(供 normalizeImages 等使用) + */ +export const optimizeImageUrls = (urls = []) => + urls.map((u) => optimizeImageUrl(u)) diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 8739fc4..d8fb7cb 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -271,7 +271,7 @@
- 封面 + 封面
@@ -613,7 +613,7 @@ @@ -628,6 +628,7 @@ import { toast } from '@/composables/useToast' import { pickMusicUrl, rememberMusicPick } from '@/utils/musicPick' import { getClientId } from '@/utils/clientId' import { sendVisitBeacon } from '@/utils/visitBeacon' +import { optimizeImageUrl } from '@/composables/useAndroidOptimize' import { DANMAKU_COLORS, DANMAKU_GRADIENT_COLORS, DEFAULT_DANMAKU_COLOR, BLESSING_STYLES } from '@/utils/danmakuColors' import { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc' import CanvasEffects from '@/components/CanvasEffects.vue' @@ -818,8 +819,8 @@ const firstScreenDuration = computed(() => { return v >= 0 && v <= 30000 ? v : 4500 }) -const curtainBg = computed(() => ({ backgroundImage: `url(${data.value.heroImg})` })) -const endImage = computed(() => data.value.endImg || data.value.heroImg) +const curtainBg = computed(() => ({ backgroundImage: `url(${optimizeImageUrl(data.value.heroImg)})` })) +const endImage = computed(() => optimizeImageUrl(data.value.endImg || data.value.heroImg)) const introExitClass = computed(() => { const effect = ['wind', 'book', 'lift', 'doors'].includes(data.value.introExitEffect) ? data.value.introExitEffect : 'wind' return `intro-exit-${effect}` @@ -871,7 +872,7 @@ const normalizeImages = (images = []) => { const seen = new Set() return images .filter((src) => typeof src === 'string' && src.trim()) - .map((src) => src.trim()) + .map((src) => optimizeImageUrl(src.trim())) .filter((src) => { if (seen.has(src)) return false seen.add(src) @@ -1318,13 +1319,28 @@ onMounted(async () => { loaded.formalPageHeight = loaded.formalPageHeight || DEFAULTS.formalPageHeight loaded.calendarDate = loaded.calendarDate || DEFAULTS.calendarDate loaded.introExitEffect = loaded.introExitEffect || DEFAULTS.introExitEffect + // 安卓专项:OSS 图统一压缩为 750px webp(iOS 不受影响) + if (loaded.heroImg) loaded.heroImg = optimizeImageUrl(loaded.heroImg) + if (loaded.endImg) loaded.endImg = optimizeImageUrl(loaded.endImg) + if (Array.isArray(loaded.photoPages)) { + loaded.photoPages = loaded.photoPages.map((p) => { + if (!p) return p + const next = { ...p } + if (next.img1) next.img1 = optimizeImageUrl(next.img1) + if (next.img2) next.img2 = optimizeImageUrl(next.img2) + if (Array.isArray(next.images)) next.images = next.images.map((u) => optimizeImageUrl(u)) + return next + }) + } data.value = { ...data.value, ...DEFAULTS, ...loaded } } } catch (e) { /* 使用默认配置 */ } syncPageScrollMode() albumLoaded.value = false - albumPreviewImages.value = localAlbumImages() + // 安卓专项:不在首屏预挂载整本相册,避免 全量创建导致主线程卡顿 + // 改为用户首次点开预览时再加载(loadAlbumPreviewImages 内部有缓存保护) + albumPreviewImages.value = [] loadLikeStatus() sendVisitBeacon()