fix: 尝试安卓优化
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
<meta name="twitter:title" content="💕李琦❤️李逸烁 💍💒婚礼请柬" />
|
||||
<meta name="twitter:description" content="💍(公历2026年9月4日)岁在丙午,仲秋之吉,四日佳期。谨订良缘,敢邀台驾。" />
|
||||
<meta property="og:image" content="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/liqi/20260731/1774761336736_compressed_compressed.webp">
|
||||
<link rel="preconnect" href="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com">
|
||||
<title>💕李琦❤️李逸烁 💒婚礼请柬</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
71
vite-tailwindcss/src/composables/useAndroidOptimize.js
Normal file
71
vite-tailwindcss/src/composables/useAndroidOptimize.js
Normal file
@@ -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))
|
||||
@@ -271,7 +271,7 @@
|
||||
<div v-reveal="{ variant: 'scale', delay: 120 }" class="w-full max-w-[260px] aspect-[3/4] relative z-10">
|
||||
<div class="absolute inset-0 border border-[#a88c6b] translate-x-3 translate-y-3 opacity-30"></div>
|
||||
<div class="relative w-full h-full z-10 bg-[#F7F6F2] shadow-md overflow-hidden">
|
||||
<img :src="data.heroImg" class="w-full h-full object-cover" fetchpriority="high" decoding="async" alt="封面" />
|
||||
<img :src="optimizeImageUrl(data.heroImg)" class="w-full h-full object-cover" fetchpriority="high" decoding="async" alt="封面" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
|
||||
<a-image-preview-group :preview="albumPreviewOptions">
|
||||
<div class="album-preview-registry" aria-hidden="true">
|
||||
<a-image v-for="(src, idx) in albumPreviewImages" :key="`${src}-${idx}`" :src="src" />
|
||||
<a-image v-for="(src, idx) in albumPreviewImages" :key="`${src}-${idx}`" :src="src" :preview="false" />
|
||||
</div>
|
||||
</a-image-preview-group>
|
||||
|
||||
@@ -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()
|
||||
// 安卓专项:不在首屏预挂载整本相册,避免 <a-image> 全量创建导致主线程卡顿
|
||||
// 改为用户首次点开预览时再加载(loadAlbumPreviewImages 内部有缓存保护)
|
||||
albumPreviewImages.value = []
|
||||
loadLikeStatus()
|
||||
sendVisitBeacon()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user