File size: 13,017 Bytes
83d8eb9
40f23a9
a1eddda
 
 
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83d8eb9
40f23a9
 
 
 
 
 
 
83d8eb9
40f23a9
 
 
 
 
 
 
 
 
 
 
83d8eb9
40f23a9
 
 
83d8eb9
 
40f23a9
0823b09
40f23a9
 
021ee94
40f23a9
 
8f2df2b
40f23a9
 
0823b09
a1eddda
40f23a9
 
 
 
 
 
 
 
 
 
021ee94
40f23a9
 
8f2df2b
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25e7862
40f23a9
25e7862
 
40f23a9
 
 
 
 
25e7862
40f23a9
25e7862
 
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25e7862
 
 
 
40f23a9
 
 
 
 
 
 
 
8f2df2b
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83d8eb9
 
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
83d8eb9
 
40f23a9
 
 
0823b09
 
 
 
 
 
 
 
40f23a9
83d8eb9
 
40f23a9
 
 
 
 
 
83d8eb9
 
0823b09
40f23a9
 
0823b09
 
 
 
 
 
 
 
 
 
 
40f23a9
0823b09
40f23a9
0823b09
 
 
 
 
 
 
 
 
 
 
40f23a9
 
 
 
 
0823b09
 
 
 
 
83d8eb9
 
40f23a9
 
 
 
83d8eb9
 
40f23a9
 
 
 
83d8eb9
 
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83d8eb9
40f23a9
83d8eb9
40f23a9
 
 
 
021ee94
40f23a9
 
 
8f2df2b
40f23a9
 
 
 
 
 
83d8eb9
25e7862
 
 
 
 
 
83d8eb9
40f23a9
 
 
 
 
 
 
 
 
83d8eb9
a1eddda
 
 
 
 
 
40f23a9
a1eddda
40f23a9
 
 
 
 
 
 
 
0823b09
 
 
 
 
 
 
 
 
25e7862
0823b09
40f23a9
83d8eb9
40f23a9
 
 
 
 
 
 
 
 
 
 
 
 
83d8eb9
a1eddda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40f23a9
 
 
 
 
 
 
 
 
 
83d8eb9
a1eddda
 
 
 
 
 
 
 
 
 
 
83d8eb9
 
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
<template>
  <div id="app">
    <!-- PC端侧边栏导航 -->
    <DesktopSidebar />

    <!-- 主内容区域 -->
    <main class="main-content">
      <router-view v-slot="{ Component, route }">
        <keep-alive>
          <component
            :is="Component"
            v-if="route.meta?.keepAlive"
            :key="route.name"
          />
        </keep-alive>
        <component
          :is="Component"
          v-if="!route.meta?.keepAlive"
          :key="route.name"
        />
      </router-view>
    </main>

    <!-- 迷你播放器 -->
    <MiniPlayer
      v-show="!shouldHideMiniPlayer"
      @openFullPlayer="showFullPlayer = true"
      @togglePlay="togglePlay"
      @playNext="playNext"
      @playPrevious="playPrevious"
    />

    <!-- 底部导航栏 -->
    <AppTabBar />

    <!-- 全屏播放器 -->
    <FullPlayerPage
      v-if="showFullPlayer"
      @close="showFullPlayer = false"
      @seek="handleSeek"
      @togglePlay="togglePlay"
    />

    <!-- 音频元素 -->
    <audio
      ref="audioRef"
      preload="metadata"
      @loadedmetadata="handleLoadedMetadata"
      @timeupdate="handleTimeUpdate"
      @ended="handleEnded"
      @play="handlePlay"
      @pause="handlePause"
      @error="handleError"
    />

    <!-- 通知组件 -->
    <Toast />
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRoute } from 'vue-router'
import { usePlayerStore } from '@/stores/player'
import { usePlayQueueStore } from '@/stores/playqueue'
import { useSearchStore } from '@/stores/search'
import { useFavoritesStore } from '@/stores/favorites'
import { useHistoryStore } from '@/stores/history'
import { useSettingsStore } from '@/stores/settings'
import { musicApi, utils } from '@/services/musicApi'
import mediaSessionManager from '@/utils/mediaSession'
import DesktopSidebar from '@/components/layout/DesktopSidebar.vue'
import AppTabBar from '@/components/layout/AppTabBar.vue'
import MiniPlayer from '@/components/layout/MiniPlayer.vue'
import FullPlayerPage from '@/views/FullPlayerPage.vue'
import Toast from '@/components/common/Toast.vue'

// Router
const route = useRoute()

// Store
const playerStore = usePlayerStore()
const playQueueStore = usePlayQueueStore()
const searchStore = useSearchStore()
const favoritesStore = useFavoritesStore()
const historyStore = useHistoryStore()
const settingsStore = useSettingsStore()

// 响应式数据
const audioRef = ref(null)
const showFullPlayer = ref(false)
const isLoading = ref(false)

// 计算属性
const currentSong = computed(() => playerStore.currentSong)
const isPlaying = computed(() => playerStore.isPlaying)

// 判断是否应该隐藏迷你播放器(在全屏播放页面时隐藏)
const shouldHideMiniPlayer = computed(() => {
  return route.meta?.fullScreen || false
})

// 播放控制
const togglePlay = async () => {
  if (!currentSong.value) {
    console.log('没有歌曲信息')
    return
  }

  if (isPlaying.value) {
    if (audioRef.value) {
      audioRef.value.pause()
    }
    playerStore.setPlayingState(false)
  } else {
    // 如果没有音频源,需要先加载歌曲
    if (!playerStore.audioSrc || !audioRef.value.src || audioRef.value.src === location.href) {
      console.log('没有音频源,开始加载歌曲')
      await loadAndPlaySong(currentSong.value)
      return
    }

    // 有音频源,直接播放
    try {
      if (!audioRef.value) {
        console.log('音频元素不可用')
        return
      }

      // 确保音频元素有正确的src
      if (audioRef.value.src !== playerStore.audioSrc) {
        audioRef.value.src = playerStore.audioSrc
        audioRef.value.load()
      }

      await audioRef.value.play()
      playerStore.setPlayingState(true)
      console.log('开始播放:', currentSong.value.name)
    } catch (error) {
      console.error('播放失败:', error)
      showNotification('播放失败,请重试', 'error')
    }
  }
}

const playNext = () => {
  const nextSong = playQueueStore.playNext()
  if (nextSong) {
    // 强制从头开始播放下一首歌曲
    playerStore.setCurrentTime(0)
    loadAndPlaySong(nextSong)
  }
}

const playPrevious = () => {
  const prevSong = playQueueStore.playPrevious()
  if (prevSong) {
    // 强制从头开始播放上一首歌曲
    playerStore.setCurrentTime(0)
    loadAndPlaySong(prevSong)
  }
}

// 快进到指定时间
const handleSeek = (time) => {
  if (audioRef.value && isFinite(time) && time >= 0 && time <= (playerStore.duration || 0)) {
    // 立即更新播放器时间
    audioRef.value.currentTime = time
    playerStore.setCurrentTime(time)

    // 如果当前没有播放,且有音频源,尝试播放
    if (!isPlaying.value && playerStore.audioSrc) {
      audioRef.value.play().then(() => {
        playerStore.setPlayingState(true)
        console.log('拖动进度条后开始播放:', time)
      }).catch(error => {
        console.log('自动播放被阻止:', error)
      })
    }

    console.log('跳转到时间:', time)
  }
}

// 加载并播放歌曲
const loadAndPlaySong = async (song) => {
  if (!song || isLoading.value) return

  isLoading.value = true

  try {
    // 获取音乐链接 - 修复参数顺序:source, id, quality
    const quality = settingsStore.getSetting('defaultQuality') || '320'
    const result = await musicApi.getMusicUrl(song.source, song.id, quality)

    if (result) {
      playerStore.setAudioSrc(result)

      // 强制重置播放进度为0(所有切歌都从头开始)
      playerStore.setCurrentTime(0)
      playerStore.setDuration(0)
      console.log('切换歌曲,强制从头开始播放:', song.name)

      // 更新音频元素
      if (audioRef.value) {
        audioRef.value.src = result
        audioRef.value.load()
      }

      // 添加到播放历史
      historyStore.addToHistory(song)

      console.log('歌曲加载成功:', song.name, result)

      // 等待音频元数据加载后再尝试播放
      if (audioRef.value) {
        const tryPlay = async () => {
          try {
            if (audioRef.value.readyState >= 1) { // 有足够数据可以开始播放
              await audioRef.value.play()
              playerStore.setPlayingState(true)
              console.log('开始播放:', song.name)
            }
          } catch (error) {
            console.log('自动播放被阻止,用户需要手动播放')
            playerStore.setPlayingState(false)
          }
        }

        if (audioRef.value.readyState >= 1) {
          tryPlay()
        } else {
          audioRef.value.addEventListener('loadeddata', tryPlay, { once: true })
        }
      }
    } else {
      showNotification('无法获取播放链接', 'error')
    }
  } catch (error) {
    console.error('加载歌曲失败:', error)
    showNotification('加载歌曲失败', 'error')
  } finally {
    isLoading.value = false
  }
}

// 音频事件处理
const handleLoadedMetadata = () => {
  if (audioRef.value) {
    const duration = audioRef.value.duration
    if (isFinite(duration)) {
      playerStore.setDuration(duration)

      // 恢复播放进度
      const savedTime = playerStore.currentTime
      if (savedTime > 0 && savedTime < duration && settingsStore.getSetting('rememberProgress')) {
        audioRef.value.currentTime = savedTime
      }
    }
  }
}

const handleTimeUpdate = () => {
  if (audioRef.value && isFinite(audioRef.value.currentTime)) {
    playerStore.setCurrentTime(audioRef.value.currentTime)
    
    // 更新Media Session位置信息
    if (settingsStore.getSetting('mediaSession') && audioRef.value.duration) {
      mediaSessionManager.setPositionState(
        audioRef.value.duration,
        audioRef.value.currentTime
      )
    }
  }
}

const handleEnded = () => {
  if (settingsStore.getSetting('autoNext')) {
    playNext()
  } else {
    playerStore.setPlayingState(false)
  }
}

const handlePlay = async () => {
  playerStore.setPlayingState(true)

  // 使用新的Media Session管理器
  if (currentSong.value && settingsStore.getSetting('mediaSession')) {
    await mediaSessionManager.setMetadata(currentSong.value, {
      onPlay: togglePlay,
      onPause: togglePlay,
      onNext: playNext,
      onPrev: playPrevious,
      onSeekTo: (time) => {
        if (audioRef.value) {
          audioRef.value.currentTime = time
          playerStore.setCurrentTime(time)
        }
      }
    })
    
    // 设置播放状态
    mediaSessionManager.setPlaybackState('playing')
    
    // 设置位置信息
    if (audioRef.value) {
      mediaSessionManager.setPositionState(
        audioRef.value.duration || 0,
        audioRef.value.currentTime || 0
      )
    }
  }
}

const handlePause = () => {
  playerStore.setPlayingState(false)
  
  // 更新Media Session播放状态
  if (settingsStore.getSetting('mediaSession')) {
    mediaSessionManager.setPlaybackState('paused')
  }
}

const handleError = (e) => {
  console.error('音频播放错误:', e)
  showNotification('播放出错,请重试', 'error')
  playerStore.setPlayingState(false)
}

// 通知函数
const showNotification = (message, type = 'info') => {
  // 这里需要实现通知组件的显示逻辑
  console.log(`${type}: ${message}`)
}

// 监听当前歌曲变化
watch(currentSong, (newSong, oldSong) => {
  if (newSong && newSong !== oldSong) {
    // 如果是页面刷新后的恢复,且已有audioSrc,则不重新加载
    if (oldSong === null && playerStore.audioSrc) {
      console.log('恢复播放会话,跳过重新加载')
      // 恢复音频元素的src
      if (audioRef.value) {
        audioRef.value.src = playerStore.audioSrc
        audioRef.value.load()
      }
      return
    }

    loadAndPlaySong(newSong)
  }
}, { immediate: true })

// 监听音量变化
watch(() => playerStore.volume, (newVolume) => {
  if (audioRef.value) {
    audioRef.value.volume = newVolume / 100
  }
}, { immediate: true })

// 组件挂载
onMounted(() => {
  // 加载所有存储状态
  playerStore.loadPlayerState()
  playQueueStore.loadQueue()
  searchStore.loadSearchSettings()
  searchStore.loadSearchHistory()
  favoritesStore.loadFavorites()
  historyStore.loadHistory()
  settingsStore.loadSettings()

  // 建立音频元素连接
  if (audioRef.value) {
    playerStore.setAudioElement(audioRef.value)
    audioRef.value.volume = (playerStore.volume || 80) / 100
  }
  
  // 监听播放器状态过期事件
  window.addEventListener('playerStateExpired', () => {
    console.log('播放器状态已过期,跳转到主页')
    router.push('/')
  })

  // 应用主题
  const theme = settingsStore.getSetting('theme') || 'light'
  settingsStore.applyTheme(theme)

  // 监听来自路由版FullPlayerPage的加载歌曲事件
  const handleLoadAndPlaySong = (event) => {
    if (event.detail?.song) {
      loadAndPlaySong(event.detail.song)
    }
  }
  
  // 监听来自侧边栏的播放控制事件
  const handleSidebarTogglePlay = () => {
    togglePlay()
  }
  
  window.addEventListener('loadAndPlaySong', handleLoadAndPlaySong)
  window.addEventListener('sidebarTogglePlay', handleSidebarTogglePlay)

  // 恢复逻辑交由watch处理,避免重复设置
  console.log('App mounted, 恢复状态:', {
    currentSong: currentSong.value?.name,
    hasAudioSrc: !!playerStore.audioSrc,
    currentTheme: theme
  })
})

// 组件卸载时清理资源
onBeforeUnmount(() => {
  // 清理Media Session资源
  mediaSessionManager.cleanup()
  
  // 移除事件监听器
  window.removeEventListener('loadAndPlaySong', () => {})
  window.removeEventListener('sidebarTogglePlay', () => {})
  window.removeEventListener('playerStateExpired', () => {})
})
</script>

<style>
.main-content {
  flex: 1;
  padding-bottom: calc(var(--tabbar-height) + var(--mini-player-height));
  overflow-y: auto;
  background: var(--bg-primary);
}

/* 当没有当前歌曲时,减少底部边距 */
.main-content:not(.has-mini-player) {
  padding-bottom: var(--tabbar-height);
  background: var(--bg-primary);
}

/* PC端布局适配 */
@media (min-width: 1024px) {
  #app {
    display: flex;
    height: 100vh;
  }
  
  .main-content {
    flex: 1;
    margin-left: 280px; /* 侧边栏宽度 */
    padding-bottom: var(--mini-player-height);
  }
  
  .main-content:not(.has-mini-player) {
    padding-bottom: 0;
  }
}

/* iOS PWA 模式适配 */
@supports (-webkit-touch-callout: none) {
  @media all and (display-mode: standalone) {
    .main-content {
      padding-bottom: calc(var(--tabbar-height) + var(--mini-player-height) + 20px);
    }
    
    .main-content:not(.has-mini-player) {
      padding-bottom: calc(var(--tabbar-height) + 20px);
    }
  }
  
  /* PC端PWA模式 */
  @media (min-width: 1024px) and (display-mode: standalone) {
    .main-content {
      padding-bottom: var(--mini-player-height);
    }
    
    .main-content:not(.has-mini-player) {
      padding-bottom: 0;
    }
  }
}
</style>