From 3b8e87eafe42d4e7002c90ff88b0e11c6d016349 Mon Sep 17 00:00:00 2001 From: rucky Date: Mon, 24 Nov 2025 00:30:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=80=82=E9=85=8D?= =?UTF-8?q?=20=E5=B1=8F=E8=94=BDweb=E6=92=AD=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SPOTIFY_FINAL_SOLUTION.md | 1 + SPOTIFY_REDIRECT_URI_FIX.md | 1 + src/services/spotifyService.ts | 76 ++-- src/views/MusicRhythm.vue | 763 +++++++++++++++++++++++++++++++-- src/views/SpotifyCallback.vue | 2 +- 5 files changed, 780 insertions(+), 63 deletions(-) diff --git a/SPOTIFY_FINAL_SOLUTION.md b/SPOTIFY_FINAL_SOLUTION.md index 9599fd3..839534b 100644 --- a/SPOTIFY_FINAL_SOLUTION.md +++ b/SPOTIFY_FINAL_SOLUTION.md @@ -148,3 +148,4 @@ onMounted(async () => { --- **问题已完全解决!** 🎵✨ + diff --git a/SPOTIFY_REDIRECT_URI_FIX.md b/SPOTIFY_REDIRECT_URI_FIX.md index c31b090..981c2e4 100644 --- a/SPOTIFY_REDIRECT_URI_FIX.md +++ b/SPOTIFY_REDIRECT_URI_FIX.md @@ -92,3 +92,4 @@ const REDIRECT_URI = window.location.origin + '/' --- **请确认 Spotify Dashboard 的 Redirect URIs 设置正确后,重新测试!** + diff --git a/src/services/spotifyService.ts b/src/services/spotifyService.ts index 3e41858..1541acc 100644 --- a/src/services/spotifyService.ts +++ b/src/services/spotifyService.ts @@ -413,11 +413,28 @@ class SpotifyService { this.logout() 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 + // recommendations API 会直接返回带有 tempo 信息的歌曲 if (allTracks.length < limit / 2) { try { const bpmRange = 10 @@ -487,7 +505,12 @@ class SpotifyService { for (const track of data.tracks) { if (!trackIds.has(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) - // Spotify API 限制:一次最多 100 个 ID + // 注意:audio-features API 可能会返回 403 错误 + // 为了保证功能可用,我们不再依赖它 + // 直接返回搜索结果,tempo 信息从 recommendations API 获取(如果有) if (allTracks.length > 0) { - try { - 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 allTracks.slice(0, limit) } return [] @@ -535,8 +531,14 @@ class SpotifyService { // 获取音频特征(包含 BPM) async getAudioFeatures(trackIds: string): Promise { - const data = await this.request(`/audio-features?ids=${trackIds}`) - return data.audio_features || [] + try { + const data = await this.request(`/audio-features?ids=${trackIds}`) + return data.audio_features || [] + } catch (error) { + console.error('获取音频特征失败:', error) + // 返回空数组,让调用方继续处理 + return [] + } } // 获取用户的播放列表 diff --git a/src/views/MusicRhythm.vue b/src/views/MusicRhythm.vue index 194777a..492d9fb 100644 --- a/src/views/MusicRhythm.vue +++ b/src/views/MusicRhythm.vue @@ -135,6 +135,26 @@ + +
+ ✅ 网页播放器已就绪 +
+
+ ⏳ 正在初始化网页播放器,请稍候... +
+ + +
+
📱 移动端使用说明
+
    +
  1. 在手机上打开 Spotify App
  2. +
  3. 播放任意歌曲(可立即暂停)
  4. +
  5. 返回网页,点击"刷新设备"按钮
  6. +
  7. 选择您的手机设备
  8. +
  9. 即可通过网页控制手机播放音乐
  10. +
+
💡 移动端浏览器不支持网页播放器,需要配合 Spotify App 使用
+
@@ -159,7 +179,13 @@ @click="selectDevice(device.id)" > - {{ 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' ? '🔊' : '🎵' + }}
{{ device.name }}
@@ -315,6 +341,88 @@
+ + + +
+ + +
+ +
+
{{ currentSpotifyTrack.name }}
+
{{ currentSpotifyTrack.artists.map(a => a.name).join(', ') }}
+
+ +
+
+ + +
+ +
+
+
+ {{ formatTime(playbackPosition) }} + {{ formatTime(playbackDuration) }} +
+
+ +
+ +
+ +
+
{{ currentSpotifyTrack.name }}
+
{{ currentSpotifyTrack.artists.map(a => a.name).join(', ') }}
+
+
+ + +
+ + + +
+ + +
+
+ {{ Math.round(currentSpotifyTrack.tempo) }} BPM +
+
+ 🎵 {{ spotifyDevices.find(d => d.id === spotifyDeviceId)?.name || 'Web Player' }} +
+
+ + + +
+
+
+
@@ -326,6 +434,11 @@ import { spotifyService, type SpotifyTrack, type SpotifyPlaylist } from '../serv const router = useRouter() const canvasRef = ref(null) +// 检测移动设备 +const isMobileDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( + navigator.userAgent.toLowerCase() +) + // BPM Presets const bpmPresets = ref([ { type: 'walk', name: '散步', bpm: 80, icon: '🚶', range: { min: 60, max: 100 } }, @@ -364,12 +477,17 @@ const spotifyPlaying = ref(false) const spotifyDeviceId = ref(null) const spotifyDevices = ref([]) const showDeviceSelector = ref(false) +const isPlayerExpanded = ref(true) // 播放器展开状态 +const playbackProgress = ref(0) // 播放进度百分比 +const playbackPosition = ref(0) // 当前播放位置(毫秒) +const playbackDuration = ref(0) // 总时长(毫秒) // Audio let audioContext: AudioContext | null = null let beatInterval: number | null = null let elapsedInterval: number | null = null let animationId: number | null = null +let progressInterval: number | null = null // Canvas let ctx: CanvasRenderingContext2D | null = null @@ -437,6 +555,28 @@ const formatDuration = (seconds: number) => { 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 = () => { isDetecting.value = !isDetecting.value @@ -630,36 +770,96 @@ const disconnectSpotify = () => { const loadSpotifyUser = async () => { try { spotifyUser.value = await spotifyService.getUserProfile() + console.log('✅ 用户信息加载成功:', spotifyUser.value?.display_name) - // 获取可用设备 - const devices = await spotifyService.getDevices() - console.log('可用设备:', devices) - - spotifyDevices.value = devices - - if (devices.length > 0) { - // 优先选择当前浏览器设备(Web Playback SDK) - let browserDevice = devices.find((d: any) => - d.name.includes('Web Player') || - d.name.includes('Browser') || - d.name.includes('Chrome') || - d.name.includes('Firefox') || - d.name.includes('Safari') - ) + if (isMobileDevice) { + // 📱 移动设备:不支持 Web Playback SDK + console.log('📱 检测到移动设备,Web Player 不受支持') + console.log('💡 请打开 Spotify 移动应用,然后点击"刷新设备"选择您的手机') - // 如果没找到浏览器设备,选择活跃设备或第一个设备 - if (!browserDevice) { - browserDevice = devices.find((d: any) => d.is_active) || devices[0] + // 显示提示信息 + setTimeout(() => { + if (!spotifyDeviceId.value) { + alert('📱 移动端使用说明\n\n由于浏览器限制,移动端无法使用网页播放器。\n\n请按以下步骤操作:\n1. 打开 Spotify 移动应用\n2. 播放任意歌曲(然后可以暂停)\n3. 返回网页点击"🔄 刷新设备"\n4. 在设备列表中选择您的手机\n5. 即可通过网页控制手机播放音乐!') + } + }, 1000) + + // 立即获取可用设备 + setTimeout(async () => { + const devices = await spotifyService.getDevices() + console.log('📱 可用设备列表:', devices) + spotifyDevices.value = devices + + if (devices.length > 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 - console.log('已选择设备:', browserDevice.name, browserDevice.id) - } else { - console.warn('未找到可用设备,请打开 Spotify 应用') - spotifyDeviceId.value = null + // 等待 2.5 秒后获取设备列表(给 Web Player 注册时间) + setTimeout(async () => { + console.log('🔄 获取所有可用设备...') + const devices = await spotifyService.getDevices() + 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) { - console.error('加载 Spotify 用户信息失败:', error) + console.error('❌ 加载 Spotify 用户信息失败:', error) } } @@ -795,6 +995,12 @@ const playSpotifyTrack = async (track: SpotifyTrack) => { }) currentSpotifyTrack.value = track spotifyPlaying.value = true + playbackDuration.value = track.duration_ms + playbackPosition.value = 0 + playbackProgress.value = 0 + + // 启动进度更新 + startProgressTracking() } catch (error: any) { console.error('播放失败:', error) @@ -813,9 +1019,11 @@ const toggleSpotifyPlayback = async () => { if (spotifyPlaying.value) { await spotifyService.pause(spotifyDeviceId.value || undefined) spotifyPlaying.value = false + stopProgressTracking() } else { await spotifyService.play({ deviceId: spotifyDeviceId.value || undefined }) spotifyPlaying.value = true + startProgressTracking() } } catch (error: any) { alert('操作失败: ' + error.message) @@ -831,6 +1039,8 @@ const nextSpotifyTrack = async () => { const state = await spotifyService.getPlaybackState() if (state && state.item) { currentSpotifyTrack.value = state.item + playbackDuration.value = state.item.duration_ms + playbackPosition.value = state.progress_ms || 0 } } catch (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 () => { try { await spotifyService.previous(spotifyDeviceId.value || undefined) @@ -850,6 +1094,8 @@ const previousSpotifyTrack = async () => { const state = await spotifyService.getPlaybackState() if (state && state.item) { currentSpotifyTrack.value = state.item + playbackDuration.value = state.item.duration_ms + playbackPosition.value = state.progress_ms || 0 } } catch (error) { console.error('获取播放状态失败:', error) @@ -877,6 +1123,7 @@ onUnmounted(() => { // 断开 Web Player spotifyService.disconnectWebPlayer() + // 清理定时器 if (audioContext) { audioContext.close() } @@ -886,6 +1133,9 @@ onUnmounted(() => { if (animationId) { cancelAnimationFrame(animationId) } + if (progressInterval) { + clearInterval(progressInterval) + } }) @@ -899,6 +1149,7 @@ onUnmounted(() => { background: linear-gradient(135deg, #0a0a14 0%, #1a1a2e 50%, #16213e 100%); color: white; padding: 20px; + padding-bottom: 110px; /* 给浮动播放器留出空间 */ overflow-x: hidden; width: 100%; max-width: 100vw; @@ -1511,6 +1762,71 @@ onUnmounted(() => { 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 { padding: 6px 12px; background: rgba(29, 185, 84, 0.2); @@ -2160,10 +2476,101 @@ onUnmounted(() => { @media (max-width: 768px) { .music-rhythm-page { padding: 15px 10px; + padding-bottom: 100px; /* 移动端浮动播放器空间 */ max-width: 100vw; 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; } @@ -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) { .visualizer-canvas { diff --git a/src/views/SpotifyCallback.vue b/src/views/SpotifyCallback.vue index 701cc1d..d15936e 100644 --- a/src/views/SpotifyCallback.vue +++ b/src/views/SpotifyCallback.vue @@ -66,7 +66,7 @@ onMounted(async () => { // 获取返回路径 const returnPath = localStorage.getItem('spotify_return_path') || '/music-rhythm' localStorage.removeItem('spotify_return_path') - router.push(returnPath) + router.replace(returnPath) }, 1000) } else { message.value = '授权失败'