diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts
index 952f8ab..90b4b96 100644
--- a/src/renderer/hooks/MusicHook.ts
+++ b/src/renderer/hooks/MusicHook.ts
@@ -60,6 +60,7 @@ const { message } = createDiscreteApi(['message']);
// 全局变量
let progressAnimationInitialized = false;
let globalAnimationFrameId: number | null = null;
+const lastSavedTime = ref(0);
// 全局停止函数
const stopProgressAnimation = () => {
@@ -104,10 +105,21 @@ const updateProgress = () => {
let currentTime;
try {
+ // 获取当前播放位置
currentTime = currentSound.seek() as number;
+ // 减少更新频率,避免频繁更新UI
+ const timeDiff = Math.abs(currentTime - nowTime.value);
+ if (timeDiff > 0.2 || Math.floor(currentTime) !== Math.floor(nowTime.value)) {
+ nowTime.value = currentTime;
+ }
+
// 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入)
- if (Math.floor(currentTime) % 2 === 0) {
+ if (
+ Math.floor(currentTime) % 2 === 0 &&
+ Math.floor(currentTime) !== Math.floor(lastSavedTime.value)
+ ) {
+ lastSavedTime.value = currentTime;
if (playerStore.playMusic && playerStore.playMusic.id) {
localStorage.setItem(
'playProgress',
@@ -140,8 +152,10 @@ const updateProgress = () => {
console.error('更新进度出错:', error);
}
- // 继续下一帧更新
- globalAnimationFrameId = requestAnimationFrame(updateProgress);
+ // 继续下一帧更新,但降低更新频率为60帧中更新10帧
+ globalAnimationFrameId = setTimeout(() => {
+ requestAnimationFrame(updateProgress);
+ }, 100) as unknown as number;
};
// 全局启动函数
@@ -326,6 +340,37 @@ const setupAudioListeners = () => {
// 清理所有事件监听器
audioService.clearAllListeners();
+ // 监听seek开始事件,立即更新UI
+ audioService.on('seek_start', (time) => {
+ // 直接更新显示位置,不检查拖动状态
+ nowTime.value = time;
+ });
+
+ // 监听seek完成事件
+ audioService.on('seek', () => {
+ try {
+ const currentSound = sound.value;
+ if (currentSound) {
+ // 立即更新显示时间,不进行任何检查
+ const currentTime = currentSound.seek() as number;
+ if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {
+ nowTime.value = currentTime;
+
+ // 检查是否需要更新歌词
+ const newIndex = getLrcIndex(nowTime.value);
+ if (newIndex !== nowIndex.value) {
+ nowIndex.value = newIndex;
+ if (isElectron && isLyricWindowOpen.value) {
+ sendLyricToWin();
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error('处理seek事件出错:', error);
+ }
+ });
+
// 立即更新一次时间和进度(解决初始化时进度条不显示问题)
const updateCurrentTimeAndDuration = () => {
const currentSound = audioService.getCurrentSound();
diff --git a/src/renderer/layout/components/PlayBar.vue b/src/renderer/layout/components/PlayBar.vue
index 79f8ddf..6bed763 100644
--- a/src/renderer/layout/components/PlayBar.vue
+++ b/src/renderer/layout/components/PlayBar.vue
@@ -28,6 +28,8 @@
:show-tooltip="showSliderTooltip"
@mouseenter="showSliderTooltip = true"
@mouseleave="showSliderTooltip = false"
+ @dragstart="handleSliderDragStart"
+ @dragend="handleSliderDragEnd"
>
@@ -199,7 +201,6 @@ import {
nowTime,
openLyric,
playMusic,
- sound,
textColors
} from '@/hooks/MusicHook';
import { useArtist } from '@/hooks/useArtist';
@@ -233,17 +234,47 @@ watch(
// 节流版本的 seek 函数
const throttledSeek = useThrottleFn((value: number) => {
- if (!sound.value) return;
- sound.value.seek(value);
+ audioService.seek(value);
nowTime.value = value;
}, 50); // 50ms 的节流延迟
+// 拖动时的临时值,避免频繁更新 nowTime 触发重渲染
+const dragValue = ref(0);
+
+// 为滑块拖动添加状态跟踪
+const isDragging = ref(false);
+
// 修改 timeSlider 计算属性
const timeSlider = computed({
- get: () => nowTime.value,
- set: throttledSeek
+ 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)}`;
};
@@ -266,9 +297,8 @@ const getVolumeIcon = computed(() => {
const volumeSlider = computed({
get: () => audioVolume.value * 100,
set: (value) => {
- if (!sound.value) return;
localStorage.setItem('volume', (value / 100).toString());
- sound.value.volume(value / 100);
+ audioService.setVolume(value / 100);
audioVolume.value = value / 100;
}
});
diff --git a/src/renderer/services/audioService.ts b/src/renderer/services/audioService.ts
index 45aaa77..917d5c7 100644
--- a/src/renderer/services/audioService.ts
+++ b/src/renderer/services/audioService.ts
@@ -37,6 +37,10 @@ class AudioService {
private retryCount = 0;
+ private seekLock = false;
+
+ private seekDebounceTimer: NodeJS.Timeout | null = null;
+
constructor() {
if ('mediaSession' in navigator) {
this.initMediaSession();
@@ -61,21 +65,22 @@ class AudioService {
navigator.mediaSession.setActionHandler('seekto', (event) => {
if (event.seekTime && this.currentSound) {
- this.currentSound.seek(event.seekTime);
+ // this.currentSound.seek(event.seekTime);
+ this.seek(event.seekTime);
}
});
navigator.mediaSession.setActionHandler('seekbackward', (event) => {
if (this.currentSound) {
const currentTime = this.currentSound.seek() as number;
- this.currentSound.seek(currentTime - (event.seekOffset || 10));
+ this.seek(currentTime - (event.seekOffset || 10));
}
});
navigator.mediaSession.setActionHandler('seekforward', (event) => {
if (this.currentSound) {
const currentTime = this.currentSound.seek() as number;
- this.currentSound.seek(currentTime + (event.seekOffset || 10));
+ this.seek(currentTime + (event.seekOffset || 10));
}
});
@@ -355,6 +360,11 @@ class AudioService {
play(url?: string, track?: SongResult, isPlay: boolean = true): Promise {
// 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放
if (this.currentSound && !url && !track) {
+ // 如果有进行中的seek操作,等待其完成
+ if (this.seekLock && this.seekDebounceTimer) {
+ clearTimeout(this.seekDebounceTimer);
+ this.seekLock = false;
+ }
this.currentSound.play();
return Promise.resolve(this.currentSound);
}
@@ -396,6 +406,11 @@ class AudioService {
// 先停止并清理现有的音频实例
if (this.currentSound) {
console.log('audioService: 停止并清理现有的音频实例');
+ // 确保任何进行中的seek操作被取消
+ if (this.seekLock && this.seekDebounceTimer) {
+ clearTimeout(this.seekDebounceTimer);
+ this.seekLock = false;
+ }
this.currentSound.stop();
this.currentSound.unload();
this.currentSound = null;
@@ -511,6 +526,11 @@ class AudioService {
stop() {
if (this.currentSound) {
try {
+ // 确保任何进行中的seek操作被取消
+ if (this.seekLock && this.seekDebounceTimer) {
+ clearTimeout(this.seekDebounceTimer);
+ this.seekLock = false;
+ }
this.currentSound.stop();
this.currentSound.unload();
} catch (error) {
@@ -534,14 +554,26 @@ class AudioService {
seek(time: number) {
if (this.currentSound) {
- this.currentSound.seek(time);
- this.updateMediaSessionPositionState();
+ try {
+ // 直接执行seek操作,避免任何过滤或判断
+ this.currentSound.seek(time);
+ // 触发seek事件
+ this.updateMediaSessionPositionState();
+ this.emit('seek', time);
+ } catch (error) {
+ console.error('Seek操作失败:', error);
+ }
}
}
pause() {
if (this.currentSound) {
try {
+ // 如果有进行中的seek操作,等待其完成
+ if (this.seekLock && this.seekDebounceTimer) {
+ clearTimeout(this.seekDebounceTimer);
+ this.seekLock = false;
+ }
this.currentSound.pause();
} catch (error) {
console.error('Error pausing audio:', error);
diff --git a/src/renderer/store/modules/player.ts b/src/renderer/store/modules/player.ts
index 0f75bdd..a72f1f2 100644
--- a/src/renderer/store/modules/player.ts
+++ b/src/renderer/store/modules/player.ts
@@ -165,13 +165,28 @@ const getSongDetail = async (playMusic: SongResult) => {
const preloadNextSong = (nextSongUrl: string) => {
try {
- if (preloadingSounds.value.length >= 2) {
+ // 清理多余的预加载实例,确保最多只有2个预加载音频
+ while (preloadingSounds.value.length >= 2) {
const oldestSound = preloadingSounds.value.shift();
if (oldestSound) {
- oldestSound.unload();
+ try {
+ oldestSound.stop();
+ oldestSound.unload();
+ } catch (e) {
+ console.error('清理预加载音频实例失败:', e);
+ }
}
}
+ // 检查这个URL是否已经在预加载列表中
+ const existingPreload = preloadingSounds.value.find(
+ (sound) => (sound as any)._src === nextSongUrl
+ );
+ if (existingPreload) {
+ console.log('该音频已在预加载列表中,跳过:', nextSongUrl);
+ return existingPreload;
+ }
+
const sound = new Howl({
src: [nextSongUrl],
html5: true,
@@ -187,7 +202,12 @@ const preloadNextSong = (nextSongUrl: string) => {
if (index > -1) {
preloadingSounds.value.splice(index, 1);
}
- sound.unload();
+ try {
+ sound.stop();
+ sound.unload();
+ } catch (e) {
+ console.error('卸载预加载音频失败:', e);
+ }
});
return sound;