File size: 6,344 Bytes
8f2df2b 021ee94 8f2df2b 021ee94 8f2df2b 021ee94 8f2df2b 9e40388 8f2df2b 9e40388 8f2df2b 9e40388 8f2df2b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { usePlayerStore } from '@/stores/player'
import { imageCacheManager } from '@/utils/imageCache'
/**
* 歌曲封面懒加载工具
* 使用 IntersectionObserver API 实现正确的图片懒加载并逐步请求
*/
export const useSongCoverLoader = () => {
const playerStore = usePlayerStore()
const observer = ref(null)
const observedImages = new Set() // 记录被观察的图片
const loadQueue = [] // 加载队列
const isProcessingQueue = ref(false) // 是否正在处理队列
/**
* 处理加载队列,逐个加载图片
*/
const processLoadQueue = async () => {
if (isProcessingQueue.value || loadQueue.length === 0) return
isProcessingQueue.value = true
while (loadQueue.length > 0) {
const { img, song } = loadQueue.shift()
// 检查元素是否还在DOM中
if (img.parentNode) {
await loadCoverForElement(img, song)
}
// 逐个加载,每次间隔200ms
if (loadQueue.length > 0) {
await new Promise(resolve => setTimeout(resolve, 200))
}
}
isProcessingQueue.value = false
}
/**
* 获取默认封面
* 返回一个1x1透明像素的data URL,避免显示浏览器默认的破损图片图标
*/
const getDefaultCover = () => {
// 返回1x1透明像素的data URL
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
}
/**
* 处理图片加载错误
*/
const handleImageError = (event) => {
// 不设置 src,也不隐藏图片,让父组件的错误处理机制接管
// 可以触发自定义事件让组件知道加载失败
event.target.dispatchEvent(new CustomEvent('cover-load-failed'))
}
/**
* 初始化懒加载观察器
*/
const initLazyLoading = () => {
if (!window.IntersectionObserver) {
console.warn('浏览器不支持 IntersectionObserver,使用立即加载')
return false
}
observer.value = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
const songData = img.dataset.song
if (songData) {
try {
const song = JSON.parse(songData)
// 添加到加载队列而非立即加载
loadQueue.push({ img, song })
// 开始处理队列
processLoadQueue()
// 停止观察该元素
observer.value.unobserve(img)
observedImages.delete(img)
} catch (error) {
console.error('解析歌曲数据失败:', error)
}
}
}
})
},
{
// 提前 100px 开始加载
rootMargin: '100px 0px',
threshold: 0.1
}
)
return true
}
/**
* 为图片元素加载封面
*/
const loadCoverForElement = async (imgElement, song) => {
if (!imgElement || !song) return
try {
// 检查元素是否还存在于DOM中
if (!imgElement.parentNode) {
return
}
// 使用新的缓存机制
const { getCachedMusicPicUrlWithDelay } = await import('@/utils/musicPicCache.js')
const coverUrl = await getCachedMusicPicUrlWithDelay(
song.source,
song.pic_id || song.id,
300
)
if (coverUrl && imgElement.parentNode) {
imgElement.src = coverUrl
}
} catch (error) {
console.error('加载封面失败:', error)
if (imgElement.parentNode) {
imgElement.src = getDefaultCover()
}
}
}
/**
* 观察图片元素进行懒加载
* @param {HTMLImageElement} imgElement - 图片元素
* @param {Object} song - 歌曲对象
*/
const observeImage = (imgElement, song) => {
if (!imgElement || !song || observedImages.has(imgElement)) return
// 设置默认封面
imgElement.src = getDefaultCover()
// 将歌曲数据存储在 data 属性中
imgElement.dataset.song = JSON.stringify(song)
// 如果观察器未初始化,先初始化
if (!observer.value && !initLazyLoading()) {
// 不支持 IntersectionObserver,加入队列逐步加载
loadQueue.push({ imgElement, song })
processLoadQueue()
return
}
// 开始观察
observer.value.observe(imgElement)
observedImages.add(imgElement)
}
/**
* 立即加载歌曲封面(不延时)
* @param {Object} song - 歌曲对象
* @param {Number} size - 图片尺寸
*/
const loadCoverImmediately = async (song, size = 300) => {
if (!song) return getDefaultCover()
try {
// 使用新的缓存机制(无延时版本)
const { getCachedMusicPicUrl } = await import('@/utils/musicPicCache.js')
const coverUrl = await getCachedMusicPicUrl(
song.source,
song.pic_id || song.id,
size
)
return coverUrl || getDefaultCover()
} catch (error) {
console.error('加载封面失败:', error)
return getDefaultCover()
}
}
/**
* 为图片元素设置封面
* @param {HTMLImageElement} imgElement - 图片元素
* @param {Object} song - 歌曲对象
* @param {Number} size - 图片尺寸
*/
const setCoverForElement = async (imgElement, song, size = 300) => {
if (!imgElement || !song) return
try {
// 使用新的缓存机制
const { getCachedMusicPicUrlWithDelay } = await import('@/utils/musicPicCache.js')
const coverUrl = await getCachedMusicPicUrlWithDelay(
song.source,
song.pic_id || song.id,
size
)
if (coverUrl) {
imgElement.src = coverUrl
}
} catch (error) {
console.error('加载封面失败:', error)
imgElement.src = getDefaultCover()
}
}
// 组件卸载时清理资源
onUnmounted(() => {
if (observer.value) {
observer.value.disconnect()
observer.value = null
}
observedImages.clear()
})
return {
getDefaultCover,
handleImageError,
observeImage,
loadCoverImmediately,
setCoverForElement
}
} |