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