@@ -135,6 +135,26 @@
< / button >
< button class = "btn-logout" @click ="disconnectSpotify" > 退出 < / button >
< / 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 >
<!-- 设备选择器 -- >
@@ -159,7 +179,13 @@
@click ="selectDevice(device.id)"
>
< 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 >
< div class = "device-info" >
< div class = "device-name" > { { device . name } } < / div >
@@ -315,6 +341,88 @@
< / 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 >
< / template >
@@ -326,6 +434,11 @@ import { spotifyService, type SpotifyTrack, type SpotifyPlaylist } from '../serv
const router = useRouter ( )
const canvasRef = ref < HTMLCanvasElement | null > ( 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 < string | null > ( null )
const spotifyDevices = ref < any [ ] > ( [ ] )
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,16 +770,77 @@ const disconnectSpotify = () => {
const loadSpotifyUser = async ( ) => {
try {
spotifyUser . value = await spotifyService . getUserProfile ( )
console . log ( '✅ 用户信息加载成功:' , spotifyUser . value ? . display _name )
// 获取可用设备
if ( isMobileDevice ) {
// 📱 移动设备:不支持 Web Playback SDK
console . log ( '📱 检测到移动设备, Web Player 不受支持' )
console . log ( '💡 请打开 Spotify 移动应用,然后点击"刷新设备"选择您的手机' )
// 显示提示信息
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 )
console . log ( '📱 可用设备列表 :' , devices )
spotifyDevices . value = devices
if ( devices . length > 0 ) {
// 优先选择当前浏览器设备( Web Playback SDK)
// 选择活跃的移动设备
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 账号' )
}
// 等待 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' ) ||
@@ -647,19 +848,18 @@ const loadSpotifyUser = async () => {
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 . id )
} else {
console . warn ( '未找到可用设备,请打开 Spotify 应用' )
spotifyDeviceId . value = null
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 )
}
} )
< / script >
@@ -899,6 +1149,7 @@ onUnmounted(() => {
background : linear - gradient ( 135 deg , # 0 a0a14 0 % , # 1 a1a2e 50 % , # 16213 e 100 % ) ;
color : white ;
padding : 20 px ;
padding - bottom : 110 px ; /* 给浮动播放器留出空间 */
overflow - x : hidden ;
width : 100 % ;
max - width : 100 vw ;
@@ -1511,6 +1762,71 @@ onUnmounted(() => {
color : # 1 DB954 ;
}
. web - player - status {
margin - top : 10 px ;
padding : 8 px 12 px ;
background : rgba ( 29 , 185 , 84 , 0.2 ) ;
border : 1 px solid rgba ( 29 , 185 , 84 , 0.4 ) ;
border - radius : 6 px ;
font - size : 12 px ;
color : # 1 DB954 ;
text - align : center ;
}
. web - player - hint {
margin - top : 10 px ;
padding : 8 px 12 px ;
background : rgba ( 255 , 193 , 7 , 0.1 ) ;
border : 1 px solid rgba ( 255 , 193 , 7 , 0.3 ) ;
border - radius : 6 px ;
font - size : 12 px ;
color : # ffc107 ;
text - align : center ;
}
. mobile - device - hint {
margin - top : 10 px ;
padding : 15 px ;
background : rgba ( 33 , 150 , 243 , 0.15 ) ;
border : 1 px solid rgba ( 33 , 150 , 243 , 0.4 ) ;
border - radius : 10 px ;
font - size : 13 px ;
color : rgba ( 255 , 255 , 255 , 0.95 ) ;
}
. hint - title {
font - weight : 600 ;
margin - bottom : 10 px ;
color : # 42 a5f5 ;
font - size : 14 px ;
}
. mobile - device - hint ol {
margin : 10 px 0 ;
padding - left : 20 px ;
color : rgba ( 255 , 255 , 255 , 0.9 ) ;
}
. mobile - device - hint li {
margin : 6 px 0 ;
line - height : 1.5 ;
}
. mobile - device - hint strong {
color : # 42 a5f5 ;
font - weight : 600 ;
}
. hint - note {
margin - top : 10 px ;
padding : 8 px 12 px ;
background : rgba ( 255 , 193 , 7 , 0.15 ) ;
border - radius : 6 px ;
font - size : 12 px ;
color : # ffc107 ;
line - height : 1.4 ;
}
. btn - refresh - device {
padding : 6 px 12 px ;
background : rgba ( 29 , 185 , 84 , 0.2 ) ;
@@ -2160,10 +2476,101 @@ onUnmounted(() => {
@ media ( max - width : 768 px ) {
. music - rhythm - page {
padding : 15 px 10 px ;
padding - bottom : 100 px ; /* 移动端浮动播放器空间 */
max - width : 100 vw ;
overflow - x : hidden ;
}
/* 浮动播放器移动端适配 */
. floating - player : not ( . minimized ) {
padding : 12 px 15 px ;
}
. player - main {
flex - direction : column ;
gap : 15 px ;
}
. player - track - info {
width : 100 % ;
gap : 12 px ;
}
. player - cover {
width : 48 px ;
height : 48 px ;
}
. player - track - name {
font - size : 14 px ;
}
. player - artist - name {
font - size : 12 px ;
}
. player - controls {
gap : 10 px ;
}
. player - btn {
width : 40 px ;
height : 40 px ;
font - size : 16 px ;
}
. player - btn - play {
width : 48 px ;
height : 48 px ;
font - size : 20 px ;
}
. player - extra {
justify - content : center ;
gap : 15 px ;
font - size : 12 px ;
}
. player - device {
max - width : 150 px ;
}
. player - minimize - btn {
width : 32 px ;
height : 32 px ;
font - size : 14 px ;
}
. progress - time {
font - size : 10 px ;
top : - 18 px ;
}
/* 收缩状态移动端 */
. player - minimized {
padding : 8 px 15 px ;
gap : 10 px ;
}
. mini - cover {
width : 40 px ;
height : 40 px ;
}
. mini - track - name {
font - size : 13 px ;
}
. mini - artist - name {
font - size : 11 px ;
}
. player - btn - mini {
width : 36 px ;
height : 36 px ;
font - size : 16 px ;
}
* {
box - sizing : border - box ;
}
@@ -2725,6 +3132,312 @@ onUnmounted(() => {
}
}
/* 浮动播放控制器 */
. floating - player {
position : fixed ;
bottom : 0 ;
left : 0 ;
right : 0 ;
background : linear - gradient ( 135 deg , rgba ( 29 , 185 , 84 , 0.95 ) , rgba ( 24 , 163 , 74 , 0.95 ) ) ;
backdrop - filter : blur ( 20 px ) ;
border - top : 2 px solid rgba ( 29 , 185 , 84 , 0.5 ) ;
box - shadow : 0 - 4 px 30 px rgba ( 0 , 0 , 0 , 0.5 ) ;
z - index : 1000 ;
transition : all 0.3 s ease ;
}
/* 展开状态 */
. floating - player : not ( . minimized ) {
padding : 15 px 30 px ;
}
/* 收缩状态 */
. floating - player . minimized {
padding : 0 ;
cursor : pointer ;
}
/* 收缩状态内容 */
. player - minimized {
position : relative ;
display : flex ;
align - items : center ;
gap : 15 px ;
padding : 10 px 20 px ;
max - width : 1600 px ;
margin : 0 auto ;
}
. mini - cover {
width : 48 px ;
height : 48 px ;
border - radius : 6 px ;
object - fit : cover ;
flex - shrink : 0 ;
}
. mini - info {
flex : 1 ;
min - width : 0 ;
}
. mini - track - name {
font - size : 14 px ;
font - weight : 600 ;
color : white ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. mini - artist - name {
font - size : 12 px ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. player - btn - mini {
width : 40 px ;
height : 40 px ;
border - radius : 50 % ;
border : none ;
background : white ;
color : # 1 DB954 ;
font - size : 18 px ;
cursor : pointer ;
display : flex ;
align - items : center ;
justify - content : center ;
transition : all 0.2 s ;
flex - shrink : 0 ;
z - index : 2 ;
}
. player - btn - mini : hover {
transform : scale ( 1.1 ) ;
}
. mini - progress {
position : absolute ;
bottom : 0 ;
left : 0 ;
height : 3 px ;
background : rgba ( 255 , 255 , 255 , 0.9 ) ;
transition : width 0.3 s ease ;
border - radius : 0 2 px 0 0 ;
}
/* 展开状态内容 */
. floating - player - content {
max - width : 1600 px ;
margin : 0 auto ;
display : flex ;
flex - direction : column ;
gap : 12 px ;
}
. player - main {
display : flex ;
align - items : center ;
gap : 30 px ;
justify - content : space - between ;
position : relative ;
}
/* 进度条 */
. player - progress - bar {
width : 100 % ;
height : 6 px ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border - radius : 3 px ;
cursor : pointer ;
position : relative ;
overflow : hidden ;
}
. progress - fill {
height : 100 % ;
background : white ;
border - radius : 3 px ;
transition : width 0.1 s linear ;
box - shadow : 0 0 8 px rgba ( 255 , 255 , 255 , 0.5 ) ;
}
. player - progress - bar : hover . progress - fill {
background : linear - gradient ( 90 deg , white , rgba ( 255 , 255 , 255 , 0.9 ) ) ;
}
. progress - time {
position : absolute ;
top : - 20 px ;
width : 100 % ;
display : flex ;
justify - content : space - between ;
font - size : 11 px ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
pointer - events : none ;
}
. player - track - info {
display : flex ;
align - items : center ;
gap : 15 px ;
flex : 1 ;
min - width : 0 ;
}
. player - cover {
width : 56 px ;
height : 56 px ;
border - radius : 8 px ;
object - fit : cover ;
box - shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.3 ) ;
flex - shrink : 0 ;
}
. player - details {
flex : 1 ;
min - width : 0 ;
}
. player - track - name {
font - size : 16 px ;
font - weight : 600 ;
color : white ;
margin - bottom : 4 px ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. player - artist - name {
font - size : 14 px ;
color : rgba ( 255 , 255 , 255 , 0.85 ) ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. player - controls {
display : flex ;
align - items : center ;
gap : 15 px ;
flex - shrink : 0 ;
}
. player - btn {
width : 44 px ;
height : 44 px ;
border - radius : 50 % ;
border : none ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
color : white ;
font - size : 18 px ;
cursor : pointer ;
display : flex ;
align - items : center ;
justify - content : center ;
transition : all 0.3 s ;
backdrop - filter : blur ( 10 px ) ;
}
. 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 : 52 px ;
height : 52 px ;
font - size : 22 px ;
background : white ;
color : # 1 DB954 ;
box - shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.2 ) ;
}
. player - btn - play : hover {
background : white ;
transform : scale ( 1.15 ) ;
box - shadow : 0 6 px 20 px rgba ( 0 , 0 , 0 , 0.3 ) ;
}
. player - extra {
display : flex ;
align - items : center ;
gap : 20 px ;
flex - shrink : 0 ;
color : white ;
font - size : 13 px ;
}
. player - bpm {
padding : 6 px 12 px ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border - radius : 20 px ;
font - weight : 600 ;
backdrop - filter : blur ( 10 px ) ;
}
. player - device {
padding : 6 px 12 px ;
background : rgba ( 0 , 0 , 0 , 0.2 ) ;
border - radius : 20 px ;
max - width : 200 px ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
backdrop - filter : blur ( 10 px ) ;
}
/* 收缩按钮 */
. player - minimize - btn {
width : 36 px ;
height : 36 px ;
border - radius : 50 % ;
border : none ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
color : white ;
font - size : 16 px ;
cursor : pointer ;
display : flex ;
align - items : center ;
justify - content : center ;
transition : all 0.2 s ;
flex - shrink : 0 ;
backdrop - filter : blur ( 10 px ) ;
}
. 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.4 s cubic - bezier ( 0.4 , 0 , 0.2 , 1 ) , opacity 0.4 s ;
}
. slide - up - enter - from {
transform : translateY ( 100 % ) ;
opacity : 0 ;
}
. slide - up - leave - to {
transform : translateY ( 100 % ) ;
opacity : 0 ;
}
/* 横屏模式优化 */
@ media ( max - width : 900 px ) and ( orientation : landscape ) {
. visualizer - canvas {