feat: 优化音乐播放器组件,增强用户体验和功能

- 在ManageLayout.vue中新增音乐播放器控制功能,包括播放、暂停、上一首、下一首等按钮
- 改进音乐播放器高度计算逻辑,确保播放器在可见时正确显示
- 添加音量控制滑块,允许用户调整音量
- 增强队列管理功能,支持清空等待队列
- 更新样式以提升播放器的视觉效果和响应式设计
This commit is contained in:
Megghy
2025-06-26 00:23:24 +08:00
parent f57c856c3b
commit 3fad3f277d

View File

@@ -23,7 +23,7 @@ import {
VideoAdd20Filled, VideoAdd20Filled,
Mail24Filled, Mail24Filled,
} from '@vicons/fluent' } from '@vicons/fluent'
import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye } from '@vicons/ionicons5' import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye, PlayForward, PlayBack, Play, Pause, VolumeHigh, ChevronUp, ChevronDown, TrashBin } from '@vicons/ionicons5'
import { useElementSize, useStorage } from '@vueuse/core' import { useElementSize, useStorage } from '@vueuse/core'
import { import {
NAlert, NAlert,
@@ -52,6 +52,7 @@ import {
NTooltip, NTooltip,
useMessage, useMessage,
NCard, NCard,
NSlider,
} from 'naive-ui' } from 'naive-ui'
import { computed, h, onMounted, ref, watch } from 'vue' import { computed, h, onMounted, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router' import { RouterLink, useRoute } from 'vue-router'
@@ -68,20 +69,57 @@ const themeType = useStorage('Settings.Theme', ThemeType.Auto)
// 侧边栏和布局相关 // 侧边栏和布局相关
const sider = ref() const sider = ref()
const { width } = useElementSize(sider) const { width } = useElementSize(sider)
const musicPlayerCardRef = ref(null)
const { height: musicPlayerCardHeight } = useElementSize(musicPlayerCardRef)
// 页面类型计算 // 页面类型计算
const type = computed(() => route.meta.danmaku ? 'danmaku' : '') const type = computed(() => route.meta.danmaku ? 'danmaku' : '')
// 音乐请求服务相关 // 音乐请求服务相关
const musicRquestStore = useMusicRequestProvider() const musicRquestStore = useMusicRequestProvider()
const aplayerHeight = computed(() =>
musicRquestStore.originMusics.length === 0 ? '0' : '80' // 优化音乐播放器高度计算逻辑
const aplayerHeight = computed(() => {
if (!isPlayerVisible.value) {
return '0'
}
// Add 16px for NCard's top/bottom margin.
return `${musicPlayerCardHeight.value + 16}`
})
// 播放器是否可见
const isPlayerVisible = computed(
() => musicRquestStore.originMusics.length > 0 || musicRquestStore.waitingMusics.length > 0
) )
// 音乐播放器相关状态
const isPlayerMinimized = useStorage('Settings.MusicPlayer.Minimized', false)
const playerVolume = computed({
get: () => musicRquestStore.settings.volume,
set: (value) => musicRquestStore.settings.volume = value
})
const aplayer = ref() const aplayer = ref()
watch(aplayer, () => { watch(aplayer, () => {
musicRquestStore.aplayerRef = aplayer.value musicRquestStore.aplayerRef = aplayer.value
}) })
// 当前播放信息
const currentPlayingInfo = computed(() => {
if (musicRquestStore.currentOriginMusic && musicRquestStore.isPlayingOrderMusic) {
return {
type: 'request',
info: `正在播放 ${musicRquestStore.currentOriginMusic.from.name} 点的歌`
}
} else if (musicRquestStore.currentMusic && musicRquestStore.currentMusic.title) {
return {
type: 'normal',
info: '正在播放背景音乐'
}
}
return null
})
// 邮箱验证相关 // 邮箱验证相关
const canResendEmail = ref(false) const canResendEmail = ref(false)
const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified) const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified)
@@ -373,6 +411,45 @@ function onNextMusic() {
musicRquestStore.nextMusic() musicRquestStore.nextMusic()
} }
// 音乐播放器控制功能
function togglePlay() {
if (aplayer.value) {
const audio = aplayer.value.audio
if (audio.paused) {
aplayer.value.play()
} else {
aplayer.value.pause()
}
}
}
function onPreviousMusic() {
if (aplayer.value) {
// 如果当前播放时间大于3秒则重新开始播放当前歌曲
if (aplayer.value.audio.currentTime > 3) {
aplayer.value.audio.currentTime = 0
} else {
// 否则播放上一首
const currentIndex = musicRquestStore.aplayerMusics.findIndex(
music => music.id === musicRquestStore.currentMusic.id
)
if (currentIndex > 0) {
musicRquestStore.currentMusic = musicRquestStore.aplayerMusics[currentIndex - 1]
aplayer.value.thenPlay()
}
}
}
}
function clearWaitingQueue() {
musicRquestStore.waitingMusics.splice(0)
message.success('已清空等待队列')
}
function togglePlayerMinimize() {
isPlayerMinimized.value = !isPlayerMinimized.value
}
// 跳转到认证页面 // 跳转到认证页面
function gotoAuthPage() { function gotoAuthPage() {
if (!accountInfo.value?.biliUserAuthInfo) { if (!accountInfo.value?.biliUserAuthInfo) {
@@ -676,40 +753,346 @@ onMounted(() => {
</NScrollbar> </NScrollbar>
<!-- 音乐播放器区域 --> <!-- 音乐播放器区域 -->
<NLayoutFooter :style="`height: ${aplayerHeight}px;overflow: auto`"> <NLayoutFooter
<div style="display: flex; align-items: center; margin: 0 10px"> v-if="isPlayerVisible"
:style="`height: ${aplayerHeight}px; overflow: hidden; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);`"
class="music-player-footer"
>
<NCard
ref="musicPlayerCardRef"
:bordered="false"
embedded
:content-style="isPlayerMinimized ? 'padding: 0' : undefined"
size="small"
class="music-player-card"
:style="`
margin: 8px;
border-radius: 12px;
backdrop-filter: blur(10px);
`"
>
<!-- 播放器头部控制栏 -->
<template #header>
<NFlex
justify="space-between"
align="center"
style="padding: 0;"
>
<NFlex
align="center"
size="small"
>
<NIcon
:component="MusicalNote"
size="16"
:style="`color: ${isDarkMode ? '#a8dadc' : '#457b9d'}`"
/>
<NText
:depth="2"
style="font-size: 13px; font-weight: 500;"
>
音乐播放器
</NText>
<NTag
v-if="currentPlayingInfo && !isPlayerMinimized"
:type="currentPlayingInfo.type === 'request' ? 'success' : 'info'"
size="small"
round
:bordered="false"
style="font-size: 11px; padding: 2px 8px;"
>
{{ currentPlayingInfo.info }}
</NTag>
<template v-if="isPlayerMinimized">
<NText
v-if="musicRquestStore.currentMusic.title"
style="font-size: 13px; max-width: 250px; margin-left: 12px"
:ellipsis="{ tooltip: true }"
>
{{ musicRquestStore.currentMusic.title }} - {{ musicRquestStore.currentMusic.artist }}
</NText>
<NText
v-else
depth="3"
style="font-size: 13px; margin-left: 12px"
>
暂无播放
</NText>
</template>
</NFlex>
<NFlex
align="center"
size="small"
>
<template v-if="isPlayerMinimized">
<NTag
v-if="musicRquestStore.waitingMusics.length > 0"
type="warning"
size="small"
round
:bordered="false"
>
{{ musicRquestStore.waitingMusics.length }}
</NTag>
<NButton
circle
size="tiny"
tertiary
:disabled="musicRquestStore.aplayerMusics.length === 0"
@click.stop="togglePlay"
>
<template #icon>
<NIcon
:component="aplayer?.audio?.paused !== false ? Play : Pause"
size="14"
/>
</template>
</NButton>
<NButton
circle
size="tiny"
tertiary
:disabled="musicRquestStore.waitingMusics.length === 0 && musicRquestStore.aplayerMusics.length <= 1"
@click.stop="onNextMusic"
>
<template #icon>
<NIcon
:component="PlayForward"
size="14"
/>
</template>
</NButton>
</template>
<NTooltip>
<template #trigger>
<NButton
:type="isPlayerMinimized ? 'primary' : 'default'"
tertiary
size="small"
circle
@click="togglePlayerMinimize"
>
<template #icon>
<NIcon :component="isPlayerMinimized ? ChevronUp : ChevronDown" />
</template>
</NButton>
</template>
{{ isPlayerMinimized ? '展开播放器' : '收起播放器' }}
</NTooltip>
</NFlex>
</NFlex>
</template>
<!-- 主播放器内容 -->
<div v-show="!isPlayerMinimized">
<NFlex
align="center"
:wrap="false"
style="gap: 12px;"
>
<!-- APlayer组件 -->
<div style="flex: 1; min-width: 280px;">
<APlayer <APlayer
v-if="musicRquestStore.aplayerMusics.length > 0"
ref="aplayer" ref="aplayer"
v-model:music="musicRquestStore.currentMusic" v-model:music="musicRquestStore.currentMusic"
v-model:volume="musicRquestStore.settings.volume" v-model:volume="playerVolume"
v-model:shuffle="musicRquestStore.settings.shuffle" v-model:shuffle="musicRquestStore.settings.shuffle"
v-model:repeat="musicRquestStore.settings.repeat" v-model:repeat="musicRquestStore.settings.repeat"
:list="musicRquestStore.aplayerMusics" :list="musicRquestStore.aplayerMusics"
:list-max-height="'200'" :list-max-height="'200'"
mutex mutex
list-folded list-folded
style="flex: 1; min-width: 400px" style="border-radius: 8px;"
@ended="musicRquestStore.onMusicEnd" @ended="musicRquestStore.onMusicEnd"
@play="musicRquestStore.onMusicPlay" @play="musicRquestStore.onMusicPlay"
/> />
<NSpace vertical> </div>
<NTag
:bordered="false" <!-- 右侧控制面板 -->
type="info" <div class="music-control-panel">
<!-- 播放控制按钮 -->
<NFlex
vertical
size="small" size="small"
align="center"
style="min-width: 100px;"
> >
队列: {{ musicRquestStore.waitingMusics.length }} <NText
</NTag> depth="3"
<NButton style="font-size: 12px; margin-bottom: 4px;"
>
播放控制
</NText>
<NFlex
size="small" size="small"
type="info" justify="center"
>
<NTooltip>
<template #trigger>
<NButton
circle
secondary
size="small"
:disabled="musicRquestStore.aplayerMusics.length === 0"
@click="onPreviousMusic"
>
<template #icon>
<NIcon :component="PlayBack" />
</template>
</NButton>
</template>
上一首 / 重播
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
type="primary"
size="small"
:disabled="musicRquestStore.aplayerMusics.length === 0"
@click="togglePlay"
>
<template #icon>
<NIcon :component="aplayer?.audio?.paused !== false ? Play : Pause" />
</template>
</NButton>
</template>
{{ aplayer?.audio?.paused !== false ? '播放' : '暂停' }}
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
circle
secondary
size="small"
:disabled="musicRquestStore.waitingMusics.length === 0 && musicRquestStore.aplayerMusics.length <= 1"
@click="onNextMusic" @click="onNextMusic"
> >
下一首 <template #icon>
<NIcon :component="PlayForward" />
</template>
</NButton> </NButton>
</NSpace> </template>
下一首
</NTooltip>
</NFlex>
</NFlex>
<!-- 队列信息和管理 -->
<NFlex
vertical
size="small"
align="center"
style="min-width: 100px;"
>
<NText
depth="3"
style="font-size: 12px; margin-bottom: 4px;"
>
队列管理
</NText>
<NFlex
vertical
size="small"
align="center"
>
<NTag
:bordered="false"
:type="musicRquestStore.waitingMusics.length > 0 ? 'warning' : 'info'"
size="small"
round
style="min-width: 80px; text-align: center;"
>
等待: {{ musicRquestStore.waitingMusics.length }}
</NTag>
<NTag
:bordered="false"
type="success"
size="small"
round
style="min-width: 80px; text-align: center;"
>
歌单: {{ musicRquestStore.originMusics.length }}
</NTag>
<NTooltip v-if="musicRquestStore.waitingMusics.length > 0">
<template #trigger>
<NButton
size="tiny"
type="error"
secondary
@click="clearWaitingQueue"
>
<template #icon>
<NIcon
:component="TrashBin"
size="12"
/>
</template>
清空队列
</NButton>
</template>
清空所有等待中的点歌
</NTooltip>
</NFlex>
</NFlex>
<!-- 音量控制 -->
<NFlex
vertical
size="small"
align="center"
style="min-width: 100px;"
>
<NFlex
align="center"
size="small"
>
<NIcon
:component="VolumeHigh"
size="14"
:depth="3"
/>
<NText
depth="3"
style="font-size: 12px;"
>
音量
</NText>
</NFlex>
<NSlider
v-model:value="playerVolume"
:min="0"
:max="1"
:step="0.01"
style="width: 80px;"
:tooltip="false"
size="small"
/>
<NText
depth="3"
style="font-size: 11px;"
>
{{ Math.round(playerVolume * 100) }}%
</NText>
</NFlex>
</div> </div>
</NFlex>
</div>
<!-- 最小化状态显示 -->
<template v-if="isPlayerMinimized">
<!-- Content is moved to the header for minimized state -->
</template>
</NCard>
</NLayoutFooter> </NLayoutFooter>
</NLayout> </NLayout>
</NLayout> </NLayout>
@@ -863,10 +1246,92 @@ onMounted(() => {
margin: 16px; margin: 16px;
} }
/* 音乐播放器样式 */
.music-player-footer {
background: var(--body-color);
}
.music-player-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.music-player-card:hover {
transform: translateY(-1px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15) !important;
}
.music-control-panel {
display: flex;
gap: 12px;
align-items: flex-start;
flex-wrap: wrap;
min-width: 300px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.music-control-panel {
min-width: auto;
flex-direction: row;
justify-content: space-around;
align-items: center;
gap: 8px;
width: 100%;
}
.music-control-panel > div {
min-width: auto !important;
flex: 1;
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
.login-card, .loading-card { .login-card, .loading-card {
width: 95%; width: 95%;
margin: 8px; margin: 8px;
} }
.music-player-card {
margin: 4px !important;
}
.music-control-panel {
flex-direction: column;
align-items: center;
gap: 6px;
width: 100%;
}
.music-control-panel > div {
width: 100% !important;
min-width: auto !important;
}
}
/* 播放器按钮悬停效果 */
.music-player-card .n-button {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.music-player-card .n-button:hover {
transform: translateY(-1px);
}
/* 音量滑块样式 */
.music-player-card .n-slider {
transition: all 0.2s ease;
}
.music-player-card .n-slider:hover {
transform: scale(1.02);
}
/* 标签动画 */
.music-player-card .n-tag {
transition: all 0.2s ease;
}
.music-player-card .n-tag:hover {
transform: scale(1.05);
} }
</style> </style>