mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-05 07:20:50 +08:00
601 lines
14 KiB
Vue
601 lines
14 KiB
Vue
<template>
|
||
<div
|
||
class="mini-play-bar"
|
||
:class="{ 'pure-mode': pureModeEnabled, 'mini-mode': settingsStore.isMiniMode }"
|
||
>
|
||
<div class="mini-bar-container">
|
||
<!-- 专辑封面 -->
|
||
<div class="album-cover" @click="setMusicFull">
|
||
<n-image
|
||
:src="getImgUrl(playMusic?.picUrl, '100y100')"
|
||
fallback-src="/placeholder.png"
|
||
class="cover-img"
|
||
preview-disabled
|
||
/>
|
||
</div>
|
||
|
||
<!-- 歌曲信息 -->
|
||
<div class="song-info" @click="setMusicFull">
|
||
<div class="song-title">{{ playMusic?.name || '未播放' }}</div>
|
||
<div class="song-artist">
|
||
<span
|
||
v-for="(artists, artistsindex) in artistList"
|
||
:key="artistsindex"
|
||
class="cursor-pointer hover:text-green-500"
|
||
@click.stop="handleArtistClick(artists.id)"
|
||
>
|
||
{{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 控制按钮区域 -->
|
||
<div class="control-buttons">
|
||
<button class="control-button previous" @click="handlePrev">
|
||
<i class="iconfont icon-prev"></i>
|
||
</button>
|
||
<button class="control-button play" @click="playMusicEvent">
|
||
<i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||
</button>
|
||
<button class="control-button next" @click="handleNext">
|
||
<i class="iconfont icon-next"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 右侧功能按钮 -->
|
||
<div class="function-buttons">
|
||
<button class="function-button">
|
||
<i
|
||
class="iconfont icon-likefill"
|
||
:class="{ 'like-active': isFavorite }"
|
||
@click="toggleFavorite"
|
||
></i>
|
||
</button>
|
||
|
||
<n-popover trigger="click" :z-index="99999999" placement="top" :show-arrow="false">
|
||
<template #trigger>
|
||
<button class="function-button" @click="mute">
|
||
<i class="iconfont" :class="getVolumeIcon"></i>
|
||
</button>
|
||
</template>
|
||
<div class="volume-slider-wrapper">
|
||
<n-slider
|
||
v-model:value="volumeSlider"
|
||
:step="0.01"
|
||
:tooltip="false"
|
||
vertical
|
||
></n-slider>
|
||
</div>
|
||
</n-popover>
|
||
|
||
<!-- 播放列表按钮 -->
|
||
<button v-if="!component" class="function-button" @click="togglePlaylist">
|
||
<i class="iconfont icon-list"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 关闭按钮 -->
|
||
<button v-if="!component" class="close-button" @click="handleClose">
|
||
<i class="iconfont ri-close-line"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div
|
||
class="progress-bar"
|
||
@click="handleProgressClick"
|
||
@mousemove="handleProgressHover"
|
||
@mouseleave="handleProgressLeave"
|
||
>
|
||
<div class="progress-track"></div>
|
||
<div class="progress-fill" :style="{ width: `${(nowTime / allTime) * 100}%` }"></div>
|
||
</div>
|
||
|
||
<!-- 播放列表 - 单独放在外层,不再使用 popover -->
|
||
<div
|
||
v-if="!component"
|
||
v-show="isPlaylistOpen"
|
||
class="playlist-container"
|
||
:class="{ 'mini-mode-list': settingsStore.isMiniMode }"
|
||
>
|
||
<n-scrollbar ref="palyListRef" class="playlist-scrollbar">
|
||
<div class="playlist-items">
|
||
<div v-for="item in playList" :key="item.id" class="music-play-list-content">
|
||
<div class="flex items-center justify-between">
|
||
<song-item :key="item.id" class="flex-1" :item="item" mini></song-item>
|
||
<div class="delete-btn" @click.stop="handleDeleteSong(item)">
|
||
<i
|
||
class="iconfont ri-delete-bin-line text-gray-400 hover:text-red-500 transition-colors"
|
||
></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-scrollbar>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, provide, ref, useTemplateRef } from 'vue';
|
||
|
||
import SongItem from '@/components/common/SongItem.vue';
|
||
import { allTime, artistList, nowTime, playMusic } from '@/hooks/MusicHook';
|
||
import { useArtist } from '@/hooks/useArtist';
|
||
import { audioService } from '@/services/audioService';
|
||
import { usePlayerStore, useSettingsStore } from '@/store';
|
||
import type { SongResult } from '@/type/music';
|
||
import { getImgUrl } from '@/utils';
|
||
|
||
const playerStore = usePlayerStore();
|
||
const settingsStore = useSettingsStore();
|
||
const { navigateToArtist } = useArtist();
|
||
|
||
withDefaults(
|
||
defineProps<{
|
||
pureModeEnabled?: boolean;
|
||
component?: boolean;
|
||
}>(),
|
||
{
|
||
component: false
|
||
}
|
||
);
|
||
|
||
// 处理关闭按钮点击
|
||
const handleClose = () => {
|
||
if (settingsStore.isMiniMode) {
|
||
window.api.restore();
|
||
}
|
||
};
|
||
|
||
// 是否播放
|
||
const play = computed(() => playerStore.play as boolean);
|
||
// 播放列表
|
||
const playList = computed(() => playerStore.playList as SongResult[]);
|
||
|
||
// 音量控制
|
||
const audioVolume = ref(
|
||
localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1
|
||
);
|
||
|
||
const volumeSlider = computed({
|
||
get: () => audioVolume.value * 100,
|
||
set: (value) => {
|
||
localStorage.setItem('volume', (value / 100).toString());
|
||
audioService.setVolume(value / 100);
|
||
audioVolume.value = value / 100;
|
||
}
|
||
});
|
||
|
||
// 音量图标
|
||
const getVolumeIcon = computed(() => {
|
||
if (audioVolume.value === 0) return 'ri-volume-mute-line';
|
||
if (audioVolume.value <= 0.5) return 'ri-volume-down-line';
|
||
return 'ri-volume-up-line';
|
||
});
|
||
|
||
// 静音
|
||
const mute = () => {
|
||
if (volumeSlider.value === 0) {
|
||
volumeSlider.value = 30;
|
||
} else {
|
||
volumeSlider.value = 0;
|
||
}
|
||
};
|
||
|
||
// 收藏相关
|
||
const isFavorite = computed(() => {
|
||
const numericId =
|
||
typeof playMusic.value.id === 'string' ? parseInt(playMusic.value.id, 10) : playMusic.value.id;
|
||
return playerStore.favoriteList.includes(numericId);
|
||
});
|
||
|
||
const toggleFavorite = async (e: Event) => {
|
||
e.stopPropagation();
|
||
const numericId =
|
||
typeof playMusic.value.id === 'string' ? parseInt(playMusic.value.id, 10) : playMusic.value.id;
|
||
|
||
if (isFavorite.value) {
|
||
playerStore.removeFromFavorite(numericId);
|
||
} else {
|
||
playerStore.addToFavorite(numericId);
|
||
}
|
||
};
|
||
|
||
// 播放列表相关
|
||
const palyListRef = useTemplateRef('palyListRef') as any;
|
||
const isPlaylistOpen = ref(false);
|
||
|
||
// 提供 openPlaylistDrawer 给子组件
|
||
provide('openPlaylistDrawer', (songId: number) => {
|
||
console.log('打开歌单抽屉', songId);
|
||
// 由于在迷你模式不处理这个功能,所以只记录日志
|
||
});
|
||
|
||
// 切换播放列表显示/隐藏
|
||
const togglePlaylist = () => {
|
||
isPlaylistOpen.value = !isPlaylistOpen.value;
|
||
console.log('切换播放列表状态', isPlaylistOpen.value);
|
||
|
||
// 调整窗口大小
|
||
if (settingsStore.isMiniMode) {
|
||
try {
|
||
if (isPlaylistOpen.value) {
|
||
// 打开播放列表时调整DOM
|
||
document.body.style.height = 'auto';
|
||
document.body.style.overflow = 'visible';
|
||
|
||
// 使用新的专用 API 调整窗口大小
|
||
if (window.api && typeof window.api.resizeMiniWindow === 'function') {
|
||
window.api.resizeMiniWindow(true);
|
||
}
|
||
} else {
|
||
// 关闭播放列表时强制调整DOM
|
||
document.body.style.height = '64px';
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// 使用新的专用 API 调整窗口大小
|
||
if (window.api && typeof window.api.resizeMiniWindow === 'function') {
|
||
window.api.resizeMiniWindow(false);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('调整窗口大小失败:', error);
|
||
}
|
||
}
|
||
|
||
// 如果打开列表,滚动到当前播放歌曲
|
||
if (isPlaylistOpen.value) {
|
||
scrollToPlayList();
|
||
}
|
||
};
|
||
|
||
const scrollToPlayList = () => {
|
||
setTimeout(() => {
|
||
const currentIndex = playerStore.playListIndex;
|
||
const itemHeight = 69; // 每个列表项的高度
|
||
palyListRef.value?.scrollTo({
|
||
top: currentIndex * itemHeight,
|
||
behavior: 'smooth'
|
||
});
|
||
}, 50);
|
||
};
|
||
|
||
const handleDeleteSong = (song: SongResult) => {
|
||
if (song.id === playMusic.value.id) {
|
||
playerStore.nextPlay();
|
||
}
|
||
playerStore.removeFromPlayList(song.id as number);
|
||
};
|
||
|
||
// 艺术家点击
|
||
const handleArtistClick = (id: number) => {
|
||
navigateToArtist(id);
|
||
};
|
||
|
||
// 进度条相关
|
||
const handleProgressClick = (e: MouseEvent) => {
|
||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||
const percent = (e.clientX - rect.left) / rect.width;
|
||
audioService.seek(allTime.value * percent);
|
||
nowTime.value = allTime.value * percent;
|
||
};
|
||
|
||
const hoverTime = ref(0);
|
||
const isHovering = ref(false);
|
||
|
||
const handleProgressHover = (e: MouseEvent) => {
|
||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||
const percent = (e.clientX - rect.left) / rect.width;
|
||
hoverTime.value = allTime.value * percent;
|
||
isHovering.value = true;
|
||
};
|
||
|
||
const handleProgressLeave = () => {
|
||
isHovering.value = false;
|
||
};
|
||
|
||
// 播放控制
|
||
const handlePrev = () => playerStore.prevPlay();
|
||
const handleNext = () => playerStore.nextPlay();
|
||
|
||
const playMusicEvent = async () => {
|
||
try {
|
||
if (!playerStore.playMusic?.id || !playerStore.playMusicUrl) {
|
||
console.warn('No valid music or URL available');
|
||
playerStore.setPlay(playerStore.playMusic);
|
||
return;
|
||
}
|
||
|
||
if (play.value) {
|
||
if (audioService.getCurrentSound()) {
|
||
audioService.pause();
|
||
playerStore.setPlayMusic(false);
|
||
}
|
||
} else {
|
||
if (audioService.getCurrentSound()) {
|
||
audioService.play();
|
||
} else {
|
||
await audioService.play(playerStore.playMusicUrl, playerStore.playMusic);
|
||
}
|
||
playerStore.setPlayMusic(true);
|
||
}
|
||
} catch (error) {
|
||
console.error('播放出错:', error);
|
||
playerStore.nextPlay();
|
||
}
|
||
};
|
||
|
||
// 切换到完整播放器
|
||
const setMusicFull = () => {
|
||
playerStore.setMusicFull(true);
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.mini-play-bar {
|
||
@apply w-full flex flex-col bg-light-200 dark:bg-dark-200 shadow-md bg-opacity-60 backdrop-blur dark:bg-opacity-60;
|
||
height: 64px;
|
||
border-radius: 8px;
|
||
position: relative;
|
||
|
||
&.mini-mode {
|
||
@apply shadow-lg;
|
||
-webkit-app-region: drag;
|
||
|
||
.mini-bar-container {
|
||
@apply px-2;
|
||
}
|
||
|
||
.song-info {
|
||
width: 120px;
|
||
|
||
.song-title {
|
||
@apply text-xs font-medium;
|
||
}
|
||
|
||
.song-artist {
|
||
@apply text-xs opacity-50;
|
||
}
|
||
}
|
||
|
||
.function-buttons {
|
||
-webkit-app-region: no-drag;
|
||
@apply space-x-1 ml-1;
|
||
|
||
.function-button {
|
||
width: 28px;
|
||
height: 28px;
|
||
|
||
.iconfont {
|
||
@apply text-base;
|
||
}
|
||
}
|
||
}
|
||
|
||
.control-buttons {
|
||
@apply mx-1 space-x-0.5;
|
||
-webkit-app-region: no-drag;
|
||
.control-button {
|
||
width: 28px;
|
||
height: 28px;
|
||
|
||
.iconfont {
|
||
@apply text-base;
|
||
}
|
||
}
|
||
}
|
||
|
||
.close-button {
|
||
-webkit-app-region: no-drag;
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
|
||
.album-cover {
|
||
@apply flex-shrink-0 mr-2;
|
||
width: 36px;
|
||
height: 36px;
|
||
-webkit-app-region: no-drag;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 2px !important;
|
||
|
||
&:hover {
|
||
height: 3px !important;
|
||
|
||
.progress-track,
|
||
.progress-fill {
|
||
height: 3px !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.mini-bar-container {
|
||
@apply flex items-center px-3 h-full relative;
|
||
}
|
||
|
||
.album-cover {
|
||
@apply flex-shrink-0 mr-3 cursor-pointer;
|
||
width: 40px;
|
||
height: 40px;
|
||
|
||
.cover-img {
|
||
@apply w-full h-full rounded-md object-cover pointer-events-none;
|
||
}
|
||
}
|
||
|
||
.song-info {
|
||
@apply flex flex-col justify-center min-w-0 flex-shrink mr-4 cursor-pointer;
|
||
width: 200px;
|
||
|
||
.song-title {
|
||
@apply text-sm font-medium truncate;
|
||
color: var(--text-color-1, #000);
|
||
}
|
||
|
||
.song-artist {
|
||
@apply text-xs truncate mt-0.5 opacity-60;
|
||
color: var(--text-color-2, #666);
|
||
}
|
||
}
|
||
|
||
.control-buttons {
|
||
@apply flex items-center space-x-1 mx-4;
|
||
}
|
||
|
||
.control-button {
|
||
@apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600;
|
||
width: 32px;
|
||
height: 32px;
|
||
|
||
&:hover {
|
||
@apply bg-gray-100 dark:bg-dark-300;
|
||
}
|
||
|
||
&.play {
|
||
@apply bg-primary text-white;
|
||
&:hover {
|
||
@apply bg-green-800;
|
||
}
|
||
}
|
||
|
||
.iconfont {
|
||
@apply text-lg;
|
||
}
|
||
}
|
||
|
||
.function-buttons {
|
||
@apply flex items-center ml-auto space-x-2;
|
||
}
|
||
|
||
.function-button {
|
||
@apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer;
|
||
width: 32px;
|
||
height: 32px;
|
||
color: var(--text-color-2, #666);
|
||
|
||
&:hover {
|
||
@apply bg-gray-100 dark:bg-dark-300;
|
||
color: var(--text-color-1, #000);
|
||
}
|
||
|
||
.iconfont {
|
||
@apply text-lg;
|
||
}
|
||
}
|
||
|
||
.close-button {
|
||
@apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer ml-2;
|
||
width: 32px;
|
||
height: 32px;
|
||
color: var(--text-color-2, #666);
|
||
|
||
&:hover {
|
||
@apply bg-gray-100 dark:bg-dark-300;
|
||
color: var(--text-color-1, #000);
|
||
}
|
||
}
|
||
|
||
.progress-bar {
|
||
@apply relative w-full cursor-pointer;
|
||
height: 2px;
|
||
|
||
&:hover {
|
||
height: 4px;
|
||
|
||
.progress-track,
|
||
.progress-fill {
|
||
height: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.progress-track {
|
||
@apply absolute inset-x-0 bottom-0 transition-all duration-200;
|
||
height: 2px;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
|
||
.dark & {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
}
|
||
|
||
.progress-fill {
|
||
@apply absolute bottom-0 left-0 transition-all duration-200;
|
||
height: 2px;
|
||
background: var(--primary-color, #18a058);
|
||
}
|
||
|
||
.like-active {
|
||
@apply text-red-500 hover:text-red-600 !important;
|
||
}
|
||
|
||
.volume-slider-wrapper {
|
||
@apply p-4 rounded-xl bg-white dark:bg-dark-100 shadow-lg;
|
||
width: 40px;
|
||
height: 160px;
|
||
}
|
||
|
||
// 播放列表样式
|
||
.playlist-container {
|
||
@apply fixed left-0 right-0 bg-white dark:bg-dark-100 overflow-hidden;
|
||
top: 64px;
|
||
height: 330px;
|
||
max-height: 330px;
|
||
|
||
&.mini-mode-list {
|
||
width: 340px;
|
||
@apply bg-opacity-90 dark:bg-opacity-90;
|
||
}
|
||
}
|
||
|
||
// 播放列表内容样式
|
||
.music-play-list-content {
|
||
@apply px-2 py-1;
|
||
|
||
.delete-btn {
|
||
@apply p-2 rounded-full transition-colors duration-200 cursor-pointer;
|
||
@apply hover:bg-red-50 dark:hover:bg-red-900/20;
|
||
|
||
.iconfont {
|
||
@apply text-lg;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 过渡动画
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.playlist-scrollbar {
|
||
height: 100%;
|
||
}
|
||
|
||
.playlist-items {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.dark {
|
||
.song-info {
|
||
.song-title {
|
||
color: var(--text-color-1, #fff);
|
||
}
|
||
|
||
.song-artist {
|
||
color: var(--text-color-2, #fff);
|
||
}
|
||
}
|
||
}
|
||
</style>
|