Files
AlgerMusicPlayer/src/renderer/components/player/MiniPlayBar.vue

601 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>