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 } }