|
|
<template> |
|
|
<div id="app"> |
|
|
|
|
|
<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' |
|
|
|
|
|
|
|
|
const route = useRoute() |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
const handleLoadAndPlaySong = (event) => { |
|
|
if (event.detail?.song) { |
|
|
loadAndPlaySong(event.detail.song) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const handleSidebarTogglePlay = () => { |
|
|
togglePlay() |
|
|
} |
|
|
|
|
|
window.addEventListener('loadAndPlaySong', handleLoadAndPlaySong) |
|
|
window.addEventListener('sidebarTogglePlay', handleSidebarTogglePlay) |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
@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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@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> |
|
|
|