移动端适配 屏蔽web播放
This commit is contained in:
@@ -148,3 +148,4 @@ onMounted(async () => {
|
|||||||
---
|
---
|
||||||
|
|
||||||
**问题已完全解决!** 🎵✨
|
**问题已完全解决!** 🎵✨
|
||||||
|
|
||||||
|
|||||||
@@ -92,3 +92,4 @@ const REDIRECT_URI = window.location.origin + '/'
|
|||||||
---
|
---
|
||||||
|
|
||||||
**请确认 Spotify Dashboard 的 Redirect URIs 设置正确后,重新测试!**
|
**请确认 Spotify Dashboard 的 Redirect URIs 设置正确后,重新测试!**
|
||||||
|
|
||||||
|
|||||||
@@ -413,11 +413,28 @@ class SpotifyService {
|
|||||||
this.logout()
|
this.logout()
|
||||||
throw new Error('授权已过期,请重新登录')
|
throw new Error('授权已过期,请重新登录')
|
||||||
}
|
}
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error?.message || '请求失败')
|
// 尝试解析错误响应
|
||||||
|
try {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error?.message || `请求失败 (${response.status})`)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`请求失败 (${response.status}: ${response.statusText})`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json()
|
// 处理 204 No Content(某些 API 如 play/pause 不返回内容)
|
||||||
|
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||||
|
return {} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有响应内容
|
||||||
|
const contentType = response.headers.get('content-type')
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索歌曲
|
// 搜索歌曲
|
||||||
@@ -467,6 +484,7 @@ class SpotifyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果关键词搜索结果太少,补充使用推荐 API
|
// 如果关键词搜索结果太少,补充使用推荐 API
|
||||||
|
// recommendations API 会直接返回带有 tempo 信息的歌曲
|
||||||
if (allTracks.length < limit / 2) {
|
if (allTracks.length < limit / 2) {
|
||||||
try {
|
try {
|
||||||
const bpmRange = 10
|
const bpmRange = 10
|
||||||
@@ -487,7 +505,12 @@ class SpotifyService {
|
|||||||
for (const track of data.tracks) {
|
for (const track of data.tracks) {
|
||||||
if (!trackIds.has(track.id)) {
|
if (!trackIds.has(track.id)) {
|
||||||
trackIds.add(track.id)
|
trackIds.add(track.id)
|
||||||
allTracks.push(track)
|
// recommendations API 已经包含一些音频特征,但不包含 tempo
|
||||||
|
// 我们使用目标 BPM 作为估计值
|
||||||
|
allTracks.push({
|
||||||
|
...track,
|
||||||
|
tempo: bpm, // 使用目标 BPM 作为估计
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,38 +519,11 @@ class SpotifyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取音频特征(BPM)
|
// 注意:audio-features API 可能会返回 403 错误
|
||||||
// Spotify API 限制:一次最多 100 个 ID
|
// 为了保证功能可用,我们不再依赖它
|
||||||
|
// 直接返回搜索结果,tempo 信息从 recommendations API 获取(如果有)
|
||||||
if (allTracks.length > 0) {
|
if (allTracks.length > 0) {
|
||||||
try {
|
return allTracks.slice(0, limit)
|
||||||
const batchSize = 50 // 使用较小的批次大小,避免 URL 过长
|
|
||||||
const tracksWithTempo: SpotifyTrack[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < allTracks.length; i += batchSize) {
|
|
||||||
const batch = allTracks.slice(i, i + batchSize)
|
|
||||||
const ids = batch.map(t => t.id).join(',')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const features = await this.getAudioFeatures(ids)
|
|
||||||
|
|
||||||
const batchWithTempo = batch.map((track, index) => ({
|
|
||||||
...track,
|
|
||||||
tempo: features[index]?.tempo || bpm,
|
|
||||||
}))
|
|
||||||
|
|
||||||
tracksWithTempo.push(...batchWithTempo)
|
|
||||||
} catch (batchError) {
|
|
||||||
console.error('Failed to get audio features for batch:', batchError)
|
|
||||||
// 如果获取特征失败,仍然返回歌曲但没有 tempo
|
|
||||||
tracksWithTempo.push(...batch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tracksWithTempo.slice(0, limit)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get audio features:', error)
|
|
||||||
return allTracks.slice(0, limit)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@@ -535,8 +531,14 @@ class SpotifyService {
|
|||||||
|
|
||||||
// 获取音频特征(包含 BPM)
|
// 获取音频特征(包含 BPM)
|
||||||
async getAudioFeatures(trackIds: string): Promise<any[]> {
|
async getAudioFeatures(trackIds: string): Promise<any[]> {
|
||||||
const data = await this.request<any>(`/audio-features?ids=${trackIds}`)
|
try {
|
||||||
return data.audio_features || []
|
const data = await this.request<any>(`/audio-features?ids=${trackIds}`)
|
||||||
|
return data.audio_features || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取音频特征失败:', error)
|
||||||
|
// 返回空数组,让调用方继续处理
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户的播放列表
|
// 获取用户的播放列表
|
||||||
|
|||||||
@@ -135,6 +135,26 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn-logout" @click="disconnectSpotify">退出</button>
|
<button class="btn-logout" @click="disconnectSpotify">退出</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 桌面端:Web Player 状态提示 -->
|
||||||
|
<div v-if="!isMobileDevice && spotifyDeviceId && spotifyDevices.find(d => d.id === spotifyDeviceId)?.name?.includes('音乐律动')" class="web-player-status">
|
||||||
|
✅ 网页播放器已就绪
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!isMobileDevice && !spotifyDeviceId && spotifyDevices.length === 0" class="web-player-hint">
|
||||||
|
⏳ 正在初始化网页播放器,请稍候...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端:使用说明 -->
|
||||||
|
<div v-if="isMobileDevice" class="mobile-device-hint">
|
||||||
|
<div class="hint-title">📱 移动端使用说明</div>
|
||||||
|
<ol>
|
||||||
|
<li>在手机上打开 <strong>Spotify App</strong></li>
|
||||||
|
<li>播放任意歌曲(可立即暂停)</li>
|
||||||
|
<li>返回网页,点击"<strong>刷新设备</strong>"按钮</li>
|
||||||
|
<li>选择您的手机设备</li>
|
||||||
|
<li>即可通过网页控制手机播放音乐</li>
|
||||||
|
</ol>
|
||||||
|
<div class="hint-note">💡 移动端浏览器不支持网页播放器,需要配合 Spotify App 使用</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设备选择器 -->
|
<!-- 设备选择器 -->
|
||||||
@@ -159,7 +179,13 @@
|
|||||||
@click="selectDevice(device.id)"
|
@click="selectDevice(device.id)"
|
||||||
>
|
>
|
||||||
<span class="device-icon">
|
<span class="device-icon">
|
||||||
{{ device.type === 'Computer' ? '💻' : device.type === 'Smartphone' ? '📱' : device.type === 'Speaker' ? '🔊' : '🎵' }}
|
{{
|
||||||
|
device.name.includes('音乐律动') || device.name.includes('Web Player') ? '🌐' :
|
||||||
|
device.type === 'Computer' ? '💻' :
|
||||||
|
device.type === 'Smartphone' ? '📱' :
|
||||||
|
device.type === 'Tablet' ? '📱' :
|
||||||
|
device.type === 'Speaker' ? '🔊' : '🎵'
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<div class="device-info">
|
<div class="device-info">
|
||||||
<div class="device-name">{{ device.name }}</div>
|
<div class="device-name">{{ device.name }}</div>
|
||||||
@@ -315,6 +341,88 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 浮动播放控制器 -->
|
||||||
|
<transition name="slide-up">
|
||||||
|
<div v-if="currentSpotifyTrack && spotifyPlaying"
|
||||||
|
class="floating-player"
|
||||||
|
:class="{ 'minimized': !isPlayerExpanded }">
|
||||||
|
|
||||||
|
<!-- 收缩状态 -->
|
||||||
|
<div v-if="!isPlayerExpanded" class="player-minimized" @click="isPlayerExpanded = true">
|
||||||
|
<img
|
||||||
|
v-if="currentSpotifyTrack.album.images[2]"
|
||||||
|
:src="currentSpotifyTrack.album.images[2].url"
|
||||||
|
:alt="currentSpotifyTrack.name"
|
||||||
|
class="mini-cover"
|
||||||
|
/>
|
||||||
|
<div class="mini-info">
|
||||||
|
<div class="mini-track-name">{{ currentSpotifyTrack.name }}</div>
|
||||||
|
<div class="mini-artist-name">{{ currentSpotifyTrack.artists.map(a => a.name).join(', ') }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="player-btn-mini" @click.stop="toggleSpotifyPlayback">
|
||||||
|
{{ spotifyPlaying ? '⏸' : '▶' }}
|
||||||
|
</button>
|
||||||
|
<div class="mini-progress" :style="{ width: playbackProgress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开状态 -->
|
||||||
|
<div v-else class="floating-player-content">
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div class="player-progress-bar" @click="seekToPosition">
|
||||||
|
<div class="progress-fill" :style="{ width: playbackProgress + '%' }"></div>
|
||||||
|
<div class="progress-time">
|
||||||
|
<span>{{ formatTime(playbackPosition) }}</span>
|
||||||
|
<span>{{ formatTime(playbackDuration) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-main">
|
||||||
|
<!-- 歌曲信息 -->
|
||||||
|
<div class="player-track-info">
|
||||||
|
<img
|
||||||
|
v-if="currentSpotifyTrack.album.images[2]"
|
||||||
|
:src="currentSpotifyTrack.album.images[2].url"
|
||||||
|
:alt="currentSpotifyTrack.name"
|
||||||
|
class="player-cover"
|
||||||
|
/>
|
||||||
|
<div class="player-details">
|
||||||
|
<div class="player-track-name">{{ currentSpotifyTrack.name }}</div>
|
||||||
|
<div class="player-artist-name">{{ currentSpotifyTrack.artists.map(a => a.name).join(', ') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放控制 -->
|
||||||
|
<div class="player-controls">
|
||||||
|
<button class="player-btn" @click="previousSpotifyTrack" title="上一首">
|
||||||
|
⏮
|
||||||
|
</button>
|
||||||
|
<button class="player-btn player-btn-play" @click="toggleSpotifyPlayback" :title="spotifyPlaying ? '暂停' : '播放'">
|
||||||
|
{{ spotifyPlaying ? '⏸' : '▶' }}
|
||||||
|
</button>
|
||||||
|
<button class="player-btn" @click="nextSpotifyTrack" title="下一首">
|
||||||
|
⏭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 额外信息 -->
|
||||||
|
<div class="player-extra">
|
||||||
|
<div v-if="currentSpotifyTrack.tempo" class="player-bpm">
|
||||||
|
{{ Math.round(currentSpotifyTrack.tempo) }} BPM
|
||||||
|
</div>
|
||||||
|
<div class="player-device">
|
||||||
|
🎵 {{ spotifyDevices.find(d => d.id === spotifyDeviceId)?.name || 'Web Player' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 收缩按钮 -->
|
||||||
|
<button class="player-minimize-btn" @click="isPlayerExpanded = false" title="收缩播放器">
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -326,6 +434,11 @@ import { spotifyService, type SpotifyTrack, type SpotifyPlaylist } from '../serv
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
// 检测移动设备
|
||||||
|
const isMobileDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
|
||||||
|
navigator.userAgent.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
// BPM Presets
|
// BPM Presets
|
||||||
const bpmPresets = ref([
|
const bpmPresets = ref([
|
||||||
{ type: 'walk', name: '散步', bpm: 80, icon: '🚶', range: { min: 60, max: 100 } },
|
{ type: 'walk', name: '散步', bpm: 80, icon: '🚶', range: { min: 60, max: 100 } },
|
||||||
@@ -364,12 +477,17 @@ const spotifyPlaying = ref(false)
|
|||||||
const spotifyDeviceId = ref<string | null>(null)
|
const spotifyDeviceId = ref<string | null>(null)
|
||||||
const spotifyDevices = ref<any[]>([])
|
const spotifyDevices = ref<any[]>([])
|
||||||
const showDeviceSelector = ref(false)
|
const showDeviceSelector = ref(false)
|
||||||
|
const isPlayerExpanded = ref(true) // 播放器展开状态
|
||||||
|
const playbackProgress = ref(0) // 播放进度百分比
|
||||||
|
const playbackPosition = ref(0) // 当前播放位置(毫秒)
|
||||||
|
const playbackDuration = ref(0) // 总时长(毫秒)
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
let audioContext: AudioContext | null = null
|
let audioContext: AudioContext | null = null
|
||||||
let beatInterval: number | null = null
|
let beatInterval: number | null = null
|
||||||
let elapsedInterval: number | null = null
|
let elapsedInterval: number | null = null
|
||||||
let animationId: number | null = null
|
let animationId: number | null = null
|
||||||
|
let progressInterval: number | null = null
|
||||||
|
|
||||||
// Canvas
|
// Canvas
|
||||||
let ctx: CanvasRenderingContext2D | null = null
|
let ctx: CanvasRenderingContext2D | null = null
|
||||||
@@ -437,6 +555,28 @@ const formatDuration = (seconds: number) => {
|
|||||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatTime = (milliseconds: number) => {
|
||||||
|
const seconds = Math.floor(milliseconds / 1000)
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const seekToPosition = async (event: MouseEvent) => {
|
||||||
|
const target = event.currentTarget as HTMLElement
|
||||||
|
const rect = target.getBoundingClientRect()
|
||||||
|
const x = event.clientX - rect.left
|
||||||
|
const percentage = x / rect.width
|
||||||
|
const position = Math.floor(playbackDuration.value * percentage)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await spotifyService.seek(position, spotifyDeviceId.value || undefined)
|
||||||
|
playbackPosition.value = position
|
||||||
|
} catch (error) {
|
||||||
|
console.error('跳转失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleDetection = () => {
|
const toggleDetection = () => {
|
||||||
isDetecting.value = !isDetecting.value
|
isDetecting.value = !isDetecting.value
|
||||||
|
|
||||||
@@ -630,36 +770,96 @@ const disconnectSpotify = () => {
|
|||||||
const loadSpotifyUser = async () => {
|
const loadSpotifyUser = async () => {
|
||||||
try {
|
try {
|
||||||
spotifyUser.value = await spotifyService.getUserProfile()
|
spotifyUser.value = await spotifyService.getUserProfile()
|
||||||
|
console.log('✅ 用户信息加载成功:', spotifyUser.value?.display_name)
|
||||||
|
|
||||||
// 获取可用设备
|
if (isMobileDevice) {
|
||||||
const devices = await spotifyService.getDevices()
|
// 📱 移动设备:不支持 Web Playback SDK
|
||||||
console.log('可用设备:', devices)
|
console.log('📱 检测到移动设备,Web Player 不受支持')
|
||||||
|
console.log('💡 请打开 Spotify 移动应用,然后点击"刷新设备"选择您的手机')
|
||||||
|
|
||||||
spotifyDevices.value = devices
|
// 显示提示信息
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!spotifyDeviceId.value) {
|
||||||
|
alert('📱 移动端使用说明\n\n由于浏览器限制,移动端无法使用网页播放器。\n\n请按以下步骤操作:\n1. 打开 Spotify 移动应用\n2. 播放任意歌曲(然后可以暂停)\n3. 返回网页点击"🔄 刷新设备"\n4. 在设备列表中选择您的手机\n5. 即可通过网页控制手机播放音乐!')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
if (devices.length > 0) {
|
// 立即获取可用设备
|
||||||
// 优先选择当前浏览器设备(Web Playback SDK)
|
setTimeout(async () => {
|
||||||
let browserDevice = devices.find((d: any) =>
|
const devices = await spotifyService.getDevices()
|
||||||
d.name.includes('Web Player') ||
|
console.log('📱 可用设备列表:', devices)
|
||||||
d.name.includes('Browser') ||
|
spotifyDevices.value = devices
|
||||||
d.name.includes('Chrome') ||
|
|
||||||
d.name.includes('Firefox') ||
|
|
||||||
d.name.includes('Safari')
|
|
||||||
)
|
|
||||||
|
|
||||||
// 如果没找到浏览器设备,选择活跃设备或第一个设备
|
if (devices.length > 0) {
|
||||||
if (!browserDevice) {
|
// 选择活跃的移动设备
|
||||||
browserDevice = devices.find((d: any) => d.is_active) || devices[0]
|
let mobileDevice = devices.find((d: any) =>
|
||||||
|
d.is_active && (d.type === 'Smartphone' || d.type === 'Tablet')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果没有,选择任何活跃设备
|
||||||
|
if (!mobileDevice) {
|
||||||
|
mobileDevice = devices.find((d: any) => d.is_active) || devices[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobileDevice) {
|
||||||
|
spotifyDeviceId.value = mobileDevice.id
|
||||||
|
console.log('🎯 已选择设备:', mobileDevice.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// 💻 桌面设备:初始化 Web Player
|
||||||
|
console.log('💻 检测到桌面设备,初始化网页播放器...')
|
||||||
|
const webPlayerDeviceId = await spotifyService.initializeWebPlayer((deviceId) => {
|
||||||
|
console.log('✅ 网页播放器初始化成功!设备ID:', deviceId)
|
||||||
|
spotifyDeviceId.value = deviceId
|
||||||
|
|
||||||
|
// 播放器就绪后,等待 2 秒再刷新设备列表
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('⏰ 等待 2 秒后刷新设备列表...')
|
||||||
|
refreshSpotifyDevices()
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (webPlayerDeviceId) {
|
||||||
|
spotifyDeviceId.value = webPlayerDeviceId
|
||||||
|
console.log('✅ 已自动选择网页播放器作为默认设备')
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 网页播放器初始化失败,可能需要 Premium 账号')
|
||||||
}
|
}
|
||||||
|
|
||||||
spotifyDeviceId.value = browserDevice.id
|
// 等待 2.5 秒后获取设备列表(给 Web Player 注册时间)
|
||||||
console.log('已选择设备:', browserDevice.name, browserDevice.id)
|
setTimeout(async () => {
|
||||||
} else {
|
console.log('🔄 获取所有可用设备...')
|
||||||
console.warn('未找到可用设备,请打开 Spotify 应用')
|
const devices = await spotifyService.getDevices()
|
||||||
spotifyDeviceId.value = null
|
console.log('📱 可用设备列表:', devices)
|
||||||
|
|
||||||
|
spotifyDevices.value = devices
|
||||||
|
|
||||||
|
// 如果还没有选中设备,尝试自动选择
|
||||||
|
if (!spotifyDeviceId.value && devices.length > 0) {
|
||||||
|
// 优先选择网页播放器
|
||||||
|
let browserDevice = devices.find((d: any) =>
|
||||||
|
d.name.includes('音乐律动') ||
|
||||||
|
d.name.includes('Web Player') ||
|
||||||
|
d.name.includes('Browser') ||
|
||||||
|
d.name.includes('Chrome') ||
|
||||||
|
d.name.includes('Firefox') ||
|
||||||
|
d.name.includes('Safari')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果没找到网页设备,选择活跃设备或第一个设备
|
||||||
|
if (!browserDevice) {
|
||||||
|
browserDevice = devices.find((d: any) => d.is_active) || devices[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
spotifyDeviceId.value = browserDevice.id
|
||||||
|
console.log('🎯 已选择设备:', browserDevice.name, '类型:', browserDevice.type)
|
||||||
|
}
|
||||||
|
}, 2500)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载 Spotify 用户信息失败:', error)
|
console.error('❌ 加载 Spotify 用户信息失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,6 +995,12 @@ const playSpotifyTrack = async (track: SpotifyTrack) => {
|
|||||||
})
|
})
|
||||||
currentSpotifyTrack.value = track
|
currentSpotifyTrack.value = track
|
||||||
spotifyPlaying.value = true
|
spotifyPlaying.value = true
|
||||||
|
playbackDuration.value = track.duration_ms
|
||||||
|
playbackPosition.value = 0
|
||||||
|
playbackProgress.value = 0
|
||||||
|
|
||||||
|
// 启动进度更新
|
||||||
|
startProgressTracking()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('播放失败:', error)
|
console.error('播放失败:', error)
|
||||||
|
|
||||||
@@ -813,9 +1019,11 @@ const toggleSpotifyPlayback = async () => {
|
|||||||
if (spotifyPlaying.value) {
|
if (spotifyPlaying.value) {
|
||||||
await spotifyService.pause(spotifyDeviceId.value || undefined)
|
await spotifyService.pause(spotifyDeviceId.value || undefined)
|
||||||
spotifyPlaying.value = false
|
spotifyPlaying.value = false
|
||||||
|
stopProgressTracking()
|
||||||
} else {
|
} else {
|
||||||
await spotifyService.play({ deviceId: spotifyDeviceId.value || undefined })
|
await spotifyService.play({ deviceId: spotifyDeviceId.value || undefined })
|
||||||
spotifyPlaying.value = true
|
spotifyPlaying.value = true
|
||||||
|
startProgressTracking()
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
alert('操作失败: ' + error.message)
|
alert('操作失败: ' + error.message)
|
||||||
@@ -831,6 +1039,8 @@ const nextSpotifyTrack = async () => {
|
|||||||
const state = await spotifyService.getPlaybackState()
|
const state = await spotifyService.getPlaybackState()
|
||||||
if (state && state.item) {
|
if (state && state.item) {
|
||||||
currentSpotifyTrack.value = state.item
|
currentSpotifyTrack.value = state.item
|
||||||
|
playbackDuration.value = state.item.duration_ms
|
||||||
|
playbackPosition.value = state.progress_ms || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取播放状态失败:', error)
|
console.error('获取播放状态失败:', error)
|
||||||
@@ -841,6 +1051,40 @@ const nextSpotifyTrack = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动播放进度跟踪
|
||||||
|
const startProgressTracking = () => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每秒更新一次播放进度
|
||||||
|
progressInterval = window.setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const state = await spotifyService.getPlaybackState()
|
||||||
|
if (state && state.is_playing) {
|
||||||
|
playbackPosition.value = state.progress_ms || 0
|
||||||
|
playbackDuration.value = state.item?.duration_ms || playbackDuration.value
|
||||||
|
playbackProgress.value = playbackDuration.value > 0
|
||||||
|
? (playbackPosition.value / playbackDuration.value) * 100
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 静默失败,不影响播放
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止播放进度跟踪
|
||||||
|
const stopProgressTracking = () => {
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval)
|
||||||
|
progressInterval = null
|
||||||
|
}
|
||||||
|
playbackProgress.value = 0
|
||||||
|
playbackPosition.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
const previousSpotifyTrack = async () => {
|
const previousSpotifyTrack = async () => {
|
||||||
try {
|
try {
|
||||||
await spotifyService.previous(spotifyDeviceId.value || undefined)
|
await spotifyService.previous(spotifyDeviceId.value || undefined)
|
||||||
@@ -850,6 +1094,8 @@ const previousSpotifyTrack = async () => {
|
|||||||
const state = await spotifyService.getPlaybackState()
|
const state = await spotifyService.getPlaybackState()
|
||||||
if (state && state.item) {
|
if (state && state.item) {
|
||||||
currentSpotifyTrack.value = state.item
|
currentSpotifyTrack.value = state.item
|
||||||
|
playbackDuration.value = state.item.duration_ms
|
||||||
|
playbackPosition.value = state.progress_ms || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取播放状态失败:', error)
|
console.error('获取播放状态失败:', error)
|
||||||
@@ -877,6 +1123,7 @@ onUnmounted(() => {
|
|||||||
// 断开 Web Player
|
// 断开 Web Player
|
||||||
spotifyService.disconnectWebPlayer()
|
spotifyService.disconnectWebPlayer()
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
if (audioContext) {
|
if (audioContext) {
|
||||||
audioContext.close()
|
audioContext.close()
|
||||||
}
|
}
|
||||||
@@ -886,6 +1133,9 @@ onUnmounted(() => {
|
|||||||
if (animationId) {
|
if (animationId) {
|
||||||
cancelAnimationFrame(animationId)
|
cancelAnimationFrame(animationId)
|
||||||
}
|
}
|
||||||
|
if (progressInterval) {
|
||||||
|
clearInterval(progressInterval)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -899,6 +1149,7 @@ onUnmounted(() => {
|
|||||||
background: linear-gradient(135deg, #0a0a14 0%, #1a1a2e 50%, #16213e 100%);
|
background: linear-gradient(135deg, #0a0a14 0%, #1a1a2e 50%, #16213e 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
padding-bottom: 110px; /* 给浮动播放器留出空间 */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
@@ -1511,6 +1762,71 @@ onUnmounted(() => {
|
|||||||
color: #1DB954;
|
color: #1DB954;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.web-player-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(29, 185, 84, 0.2);
|
||||||
|
border: 1px solid rgba(29, 185, 84, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1DB954;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-player-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffc107;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(33, 150, 243, 0.15);
|
||||||
|
border: 1px solid rgba(33, 150, 243, 0.4);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #42a5f5;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device-hint ol {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device-hint li {
|
||||||
|
margin: 6px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device-hint strong {
|
||||||
|
color: #42a5f5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255, 193, 7, 0.15);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffc107;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-refresh-device {
|
.btn-refresh-device {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(29, 185, 84, 0.2);
|
background: rgba(29, 185, 84, 0.2);
|
||||||
@@ -2160,10 +2476,101 @@ onUnmounted(() => {
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.music-rhythm-page {
|
.music-rhythm-page {
|
||||||
padding: 15px 10px;
|
padding: 15px 10px;
|
||||||
|
padding-bottom: 100px; /* 移动端浮动播放器空间 */
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 浮动播放器移动端适配 */
|
||||||
|
.floating-player:not(.minimized) {
|
||||||
|
padding: 12px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-main {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-track-info {
|
||||||
|
width: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-cover {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-track-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-artist-name {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-play {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-extra {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-device {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-minimize-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-time {
|
||||||
|
font-size: 10px;
|
||||||
|
top: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收缩状态移动端 */
|
||||||
|
.player-minimized {
|
||||||
|
padding: 8px 15px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cover {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-track-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-artist-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-mini {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -2725,6 +3132,312 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 浮动播放控制器 */
|
||||||
|
.floating-player {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(29, 185, 84, 0.95), rgba(24, 163, 74, 0.95));
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-top: 2px solid rgba(29, 185, 84, 0.5);
|
||||||
|
box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开状态 */
|
||||||
|
.floating-player:not(.minimized) {
|
||||||
|
padding: 15px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收缩状态 */
|
||||||
|
.floating-player.minimized {
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收缩状态内容 */
|
||||||
|
.player-minimized {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cover {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-track-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-artist-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-mini {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #1DB954;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-mini:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 0 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开状态内容 */
|
||||||
|
.floating-player-content {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条 */
|
||||||
|
.player-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-progress-bar:hover .progress-fill {
|
||||||
|
background: linear-gradient(90deg, white, rgba(255, 255, 255, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-time {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-track-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-cover {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-track-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-artist-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-play {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
font-size: 22px;
|
||||||
|
background: white;
|
||||||
|
color: #1DB954;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-btn-play:hover {
|
||||||
|
background: white;
|
||||||
|
transform: scale(1.15);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-extra {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-bpm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-device {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收缩按钮 */
|
||||||
|
.player-minimize-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-minimize-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-minimize-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
.slide-up-enter-active,
|
||||||
|
.slide-up-leave-active {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-enter-from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 横屏模式优化 */
|
/* 横屏模式优化 */
|
||||||
@media (max-width: 900px) and (orientation: landscape) {
|
@media (max-width: 900px) and (orientation: landscape) {
|
||||||
.visualizer-canvas {
|
.visualizer-canvas {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ onMounted(async () => {
|
|||||||
// 获取返回路径
|
// 获取返回路径
|
||||||
const returnPath = localStorage.getItem('spotify_return_path') || '/music-rhythm'
|
const returnPath = localStorage.getItem('spotify_return_path') || '/music-rhythm'
|
||||||
localStorage.removeItem('spotify_return_path')
|
localStorage.removeItem('spotify_return_path')
|
||||||
router.push(returnPath)
|
router.replace(returnPath)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
message.value = '授权失败'
|
message.value = '授权失败'
|
||||||
|
|||||||
Reference in New Issue
Block a user