music / src /composables /useSongCoverLoader.js
ahutchen's picture
feat(component): 新增歌单相关组件和功能
021ee94
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
}
}