refactor(player): 提取播放栏共享逻辑为 composable

- 新增 useVolumeControl:统一音量管理(volumeSlider、mute、滚轮调节)
- 新增 useFavorite:收藏状态与切换
- 新增 usePlaybackControl:播放/暂停、上/下一首
- PlayBar、MiniPlayBar、SimplePlayBar、MobilePlayBar 使用新 composable
- 修复音量存储不一致:MiniPlayBar/SimplePlayBar 原先绕过 playerStore 直接操作 localStorage
This commit is contained in:
alger
2026-03-29 14:04:39 +08:00
parent e032afeae8
commit eaf1636505
7 changed files with 170 additions and 246 deletions

View File

@@ -129,6 +129,9 @@ 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 { useFavorite } from '@/hooks/useFavorite';
import { usePlaybackControl } from '@/hooks/usePlaybackControl';
import { useVolumeControl } from '@/hooks/useVolumeControl';
import { audioService } from '@/services/audioService';
import { usePlayerStore, useSettingsStore } from '@/store';
import type { SongResult } from '@/types/music';
@@ -138,6 +141,15 @@ const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const { navigateToArtist } = useArtist();
// 播放控制
const { isPlaying: play, playMusicEvent, handleNext, handlePrev } = usePlaybackControl();
// 音量控制(统一通过 playerStore 管理)
const { volumeSlider, volumeIcon: getVolumeIcon, mute, handleVolumeWheel } = useVolumeControl();
// 收藏
const { isFavorite, toggleFavorite } = useFavorite();
withDefaults(
defineProps<{
pureModeEnabled?: boolean;
@@ -155,66 +167,9 @@ const handleClose = () => {
}
};
// 是否播放
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 handleVolumeWheel = (e: WheelEvent) => {
// 向上滚动增加音量,向下滚动减少音量
const delta = e.deltaY < 0 ? 5 : -5;
const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);
volumeSlider.value = newValue;
};
// 收藏相关
const isFavorite = computed(() => {
return playerStore.favoriteList.includes(playMusic.value.id);
});
const toggleFavorite = async (e: Event) => {
e.stopPropagation();
let favoriteId = playMusic.value.id;
if (isFavorite.value) {
playerStore.removeFromFavorite(favoriteId);
} else {
playerStore.addToFavorite(favoriteId);
}
};
// 播放列表相关
const palyListRef = useTemplateRef('palyListRef') as any;
const isPlaylistOpen = ref(false);
@@ -308,19 +263,6 @@ const handleProgressLeave = () => {
isHovering.value = false;
};
// 播放控制
const handlePrev = () => playerStore.prevPlay();
const handleNext = () => playerStore.nextPlay();
const playMusicEvent = async () => {
try {
playerStore.setPlay(playerStore.playMusic);
} catch (error) {
console.error('播放出错:', error);
playerStore.nextPlay();
}
};
// 切换到完整播放器
const setMusicFull = () => {
playerStore.setMusicFull(true);

View File

@@ -62,10 +62,11 @@
<script lang="ts" setup>
import { useSwipe } from '@vueuse/core';
import type { Ref } from 'vue';
import { computed, inject, onMounted, ref, watch } from 'vue';
import { inject, onMounted, ref, watch } from 'vue';
import MusicFullWrapper from '@/components/lyric/MusicFullWrapper.vue';
import { artistList, playMusic, textColors } from '@/hooks/MusicHook';
import { usePlaybackControl } from '@/hooks/usePlaybackControl';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { getImgUrl, setAnimationClass } from '@/utils';
@@ -75,24 +76,15 @@ const shouldShowMobileMenu = inject('shouldShowMobileMenu') as Ref<boolean>;
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
// 是否播放
const play = computed(() => playerStore.isPlay);
// 播放控制
const { isPlaying: play, playMusicEvent, handleNext, handlePrev } = usePlaybackControl();
// 背景颜色
const background = ref('#000');
// 播放控制
function handleNext() {
playerStore.nextPlay();
}
function handlePrev() {
playerStore.prevPlay();
}
// 全屏播放器
const MusicFullRef = ref<any>(null);
// 设置musicFull
const setMusicFull = () => {
playerStore.setMusicFull(!playerStore.musicFull);
if (playerStore.musicFull) {
@@ -107,21 +99,10 @@ watch(
}
);
// 打开播放列表抽屉
const openPlayListDrawer = () => {
playerStore.setPlayListDrawerVisible(true);
};
// 播放暂停按钮事件
const playMusicEvent = async () => {
try {
playerStore.setPlay(playMusic.value);
} catch (error) {
console.error('播放出错:', error);
playerStore.nextPlay();
}
};
// 滑动切歌
const playBarRef = ref<HTMLElement | null>(null);
onMounted(() => {

View File

@@ -164,7 +164,6 @@
<script lang="ts" setup>
import { useThrottleFn } from '@vueuse/core';
import { useMessage } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
@@ -182,7 +181,10 @@ import {
textColors
} from '@/hooks/MusicHook';
import { useArtist } from '@/hooks/useArtist';
import { useFavorite } from '@/hooks/useFavorite';
import { usePlaybackControl } from '@/hooks/usePlaybackControl';
import { usePlayMode } from '@/hooks/usePlayMode';
import { useVolumeControl } from '@/hooks/useVolumeControl';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
@@ -191,9 +193,22 @@ import { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } fr
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const { t } = useI18n();
const message = useMessage();
// 是否播放
const play = computed(() => playerStore.isPlay);
// 播放控制
const { isPlaying: play, playMusicEvent, handleNext, handlePrev } = usePlaybackControl();
// 音量控制
const { volumeSlider, volumeIcon: getVolumeIcon, mute, handleVolumeWheel } = useVolumeControl();
// 收藏
const { isFavorite, toggleFavorite } = useFavorite();
// 播放模式
const { playMode, playModeIcon, playModeText, togglePlayMode } = usePlayMode();
// 播放速度控制
const { playbackRate } = storeToRefs(playerStore);
// 背景颜色
const background = ref('#000');
@@ -211,115 +226,41 @@ watch(
const throttledSeek = useThrottleFn((value: number) => {
audioService.seek(value);
nowTime.value = value;
}, 50); // 50ms 的节流延迟
}, 50);
// 拖动时的临时值,避免频繁更新 nowTime 触发重渲染
// 拖动时的临时值
const dragValue = ref(0);
// 为滑块拖动添加状态跟踪
const isDragging = ref(false);
// 修改 timeSlider 计算属性
const timeSlider = computed({
get: () => (isDragging.value ? dragValue.value : nowTime.value),
set: (value) => {
if (isDragging.value) {
// 拖动中只更新临时值,不触发 nowTime 更新和 seek 操作
dragValue.value = value;
return;
}
// 点击操作 (非拖动),可以直接 seek
throttledSeek(value);
}
});
// 添加滑块拖动开始和结束事件处理
const handleSliderDragStart = () => {
isDragging.value = true;
// 初始化拖动值为当前时间
dragValue.value = nowTime.value;
};
const handleSliderDragEnd = () => {
isDragging.value = false;
// 直接应用最终的拖动值
audioService.seek(dragValue.value);
nowTime.value = dragValue.value;
};
// 格式化提示文本,根据拖动状态显示不同的时间
const formatTooltip = (value: number) => {
return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`;
};
// 音量条 - 使用 playerStore 的统一音量管理
const getVolumeIcon = computed(() => {
// 0 静音 ri-volume-mute-line 0.5 ri-volume-down-line 1 ri-volume-up-line
if (playerStore.volume === 0) {
return 'ri-volume-mute-line';
}
if (playerStore.volume <= 0.5) {
return 'ri-volume-down-line';
}
return 'ri-volume-up-line';
});
const volumeSlider = computed({
get: () => playerStore.volume * 100,
set: (value) => {
playerStore.setVolume(value / 100);
}
});
// 静音
const mute = () => {
if (volumeSlider.value === 0) {
volumeSlider.value = 30;
} else {
volumeSlider.value = 0;
}
};
// 鼠标滚轮调整音量
const handleVolumeWheel = (e: WheelEvent) => {
// 向上滚动增加音量,向下滚动减少音量
const delta = e.deltaY < 0 ? 5 : -5;
const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);
volumeSlider.value = newValue;
};
// 播放模式
const { playMode, playModeIcon, playModeText, togglePlayMode } = usePlayMode();
// 播放速度控制
const { playbackRate } = storeToRefs(playerStore);
function handleNext() {
playerStore.nextPlay();
}
function handlePrev() {
playerStore.prevPlay();
}
const MusicFullRef = ref<any>(null);
const showSliderTooltip = ref(false);
// 播放暂停按钮事件
const playMusicEvent = async () => {
try {
const result = await playerStore.setPlay({ ...playMusic.value });
if (result) {
playerStore.setPlayMusic(true);
}
} catch (error) {
console.error('重新获取播放链接失败:', error);
message.error(t('player.playFailed'));
}
};
const musicFullVisible = computed({
get: () => playerStore.musicFull,
set: (value) => {
@@ -327,7 +268,6 @@ const musicFullVisible = computed({
}
});
// 设置musicFull
const setMusicFull = () => {
musicFullVisible.value = !musicFullVisible.value;
playerStore.setMusicFull(musicFullVisible.value);
@@ -336,24 +276,6 @@ const setMusicFull = () => {
}
};
const isFavorite = computed(() => {
if (!playMusic || !playMusic.value) return false;
return playerStore.favoriteList.includes(playMusic.value.id);
});
const toggleFavorite = async (e: Event) => {
console.log('playMusic.value', playMusic.value);
e.stopPropagation();
let favoriteId = playMusic.value.id;
if (isFavorite.value) {
playerStore.removeFromFavorite(favoriteId);
} else {
playerStore.addToFavorite(favoriteId);
}
};
const openLyricWindow = () => {
openLyric();
};
@@ -365,7 +287,6 @@ const handleArtistClick = (id: number) => {
navigateToArtist(id);
};
// 打开播放列表抽屉
const openPlayListDrawer = () => {
playerStore.setPlayListDrawerVisible(true);
};

View File

@@ -80,8 +80,10 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { allTime, nowTime, playMusic } from '@/hooks/MusicHook';
import { allTime, nowTime } from '@/hooks/MusicHook';
import { usePlaybackControl } from '@/hooks/usePlaybackControl';
import { usePlayMode } from '@/hooks/usePlayMode';
import { useVolumeControl } from '@/hooks/useVolumeControl';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { secondToMinute } from '@/utils';
@@ -98,61 +100,14 @@ const props = withDefaults(
const playerStore = usePlayerStore();
const playBarRef = ref<HTMLElement | null>(null);
// 播放状态
const play = computed(() => playerStore.isPlay);
// 播放控制
const { isPlaying: play, playMusicEvent, handleNext, handlePrev } = usePlaybackControl();
// 播放模式
const { playMode, playModeIcon, togglePlayMode } = usePlayMode();
// 音量控制
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 handleVolumeWheel = (e: WheelEvent) => {
const delta = e.deltaY < 0 ? 5 : -5;
const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);
volumeSlider.value = newValue;
};
// 播放控制
const handlePrev = () => playerStore.prevPlay();
const handleNext = () => playerStore.nextPlay();
const playMusicEvent = async () => {
try {
await playerStore.setPlay({ ...playMusic.value });
} catch (error) {
console.error('播放出错:', error);
playerStore.nextPlay();
}
};
// 音量控制(统一通过 playerStore 管理)
const { volumeSlider, volumeIcon: getVolumeIcon, mute, handleVolumeWheel } = useVolumeControl();
// 进度条控制
const isDragging = ref(false);

View File

@@ -0,0 +1,35 @@
import { computed } from 'vue';
import { playMusic } from '@/hooks/MusicHook';
import { usePlayerStore } from '@/store/modules/player';
/**
* 当前歌曲的收藏状态管理 composable
*/
export function useFavorite() {
const playerStore = usePlayerStore();
/** 当前歌曲是否已收藏 */
const isFavorite = computed(() => {
if (!playMusic?.value?.id) return false;
return playerStore.favoriteList.includes(playMusic.value.id);
});
/** 切换收藏状态 */
const toggleFavorite = (e?: Event) => {
e?.stopPropagation();
if (!playMusic?.value?.id) return;
const favoriteId = playMusic.value.id;
if (isFavorite.value) {
playerStore.removeFromFavorite(favoriteId);
} else {
playerStore.addToFavorite(favoriteId);
}
};
return {
isFavorite,
toggleFavorite
};
}

View File

@@ -0,0 +1,41 @@
import { computed } from 'vue';
import { playMusic } from '@/hooks/MusicHook';
import { usePlayerStore } from '@/store/modules/player';
/**
* 播放控制 composable播放/暂停、上一首、下一首)
*/
export function usePlaybackControl() {
const playerStore = usePlayerStore();
/** 是否正在播放 */
const isPlaying = computed(() => playerStore.isPlay);
/** 播放/暂停切换 */
const playMusicEvent = async () => {
try {
await playerStore.setPlay({ ...playMusic.value });
} catch (error) {
console.error('播放出错:', error);
playerStore.nextPlay();
}
};
/** 下一首 */
const handleNext = () => {
playerStore.nextPlay();
};
/** 上一首 */
const handlePrev = () => {
playerStore.prevPlay();
};
return {
isPlaying,
playMusicEvent,
handleNext,
handlePrev
};
}

View File

@@ -0,0 +1,49 @@
import { computed } from 'vue';
import { usePlayerStore } from '@/store/modules/player';
/**
* 统一的音量控制 composable
* 通过 playerStore 管理音量,确保所有播放栏组件的音量状态一致
*/
export function useVolumeControl() {
const playerStore = usePlayerStore();
/** 音量滑块值 (0-100) */
const volumeSlider = computed({
get: () => playerStore.volume * 100,
set: (value: number) => {
playerStore.setVolume(value / 100);
}
});
/** 音量图标 class */
const volumeIcon = computed(() => {
if (playerStore.volume === 0) return 'ri-volume-mute-line';
if (playerStore.volume <= 0.5) return 'ri-volume-down-line';
return 'ri-volume-up-line';
});
/** 静音切换 (0 ↔ 30%) */
const mute = () => {
if (volumeSlider.value === 0) {
volumeSlider.value = 30;
} else {
volumeSlider.value = 0;
}
};
/** 鼠标滚轮调整音量 ±5% */
const handleVolumeWheel = (e: WheelEvent) => {
const delta = e.deltaY < 0 ? 5 : -5;
const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);
volumeSlider.value = newValue;
};
return {
volumeSlider,
volumeIcon,
mute,
handleVolumeWheel
};
}