5 Commits

Author SHA1 Message Date
algerkong
7891bf45fd feat: 更新版本至 4.8.2 2025-06-28 18:16:19 +08:00
algerkong
2f339b1373 🐞 fix: 修复在歌词界面添加到歌单抽屉被遮挡问题 2025-06-28 17:46:24 +08:00
algerkong
749a2a69c4 feat: 重新设计歌词页面的迷你播放栏 2025-06-28 17:40:57 +08:00
algerkong
5b97010b32 🐞 fix: 修复解析错误问题, 优化播放效果 2025-06-28 17:31:37 +08:00
algerkong
694dff425b feat: 添加清除自定义音源功能 2025-06-28 17:26:07 +08:00
15 changed files with 859 additions and 98 deletions

View File

@@ -1,15 +1,16 @@
# 更新日志
## v4.8.2
### 🎨 优化
- 重新设计pc端歌词页面Mini播放栏
- 添加清除歌曲自定义解析功能
### 🐛 Bug 修复
- 修复歌曲单独解析失败问题
- 修复歌词页面加入歌单抽屉被遮挡问题
## v4.8.1
> 如果更新遇到问题,请前往 <a href="http://donate.alger.fun/download" target="_blank">下载 AlgerMusicPlayer</a>
> 请我喝咖啡(支持作者) ☕️ <a href="http://donate.alger.fun/donate" target="_blank" style="color: red; font-weight: bold;">赏你</a>
> 帮我点个 star <a href="https://github.com/algerkong/AlgerMusicPlayer" target="_blank">github star</a>
> 微信公众号 微信搜索 <span style="font-weight: bold;">AlgerMusic</span>
> QQ频道 AlgerMusic <a href="https://pd.qq.com/s/cs056n33q?b=5" target="_blank">加入频道</a>
### 🐛 Bug 修复
- 修复无法快捷键调整问题

View File

@@ -1,6 +1,6 @@
{
"name": "AlgerMusicPlayer",
"version": "4.8.1",
"version": "4.8.2",
"description": "Alger Music Player",
"author": "Alger <algerkc@qq.com>",
"main": "./out/main/index.js",

View File

@@ -38,7 +38,8 @@ export default {
failed: 'Reparse failed',
warning: 'Please select a music source',
bilibiliNotSupported: 'Bilibili videos do not support reparsing',
processing: 'Processing...'
processing: 'Processing...',
clear: 'Clear Custom Source'
},
playBar: {
expand: 'Expand Lyrics',

View File

@@ -38,7 +38,8 @@ export default {
failed: '重新解析失败',
warning: '请选择一个音源',
bilibiliNotSupported: 'B站视频不支持重新解析',
processing: '解析中...'
processing: '解析中...',
clear: '清除自定义音源'
},
playBar: {
expand: '展开歌词',

View File

@@ -32,6 +32,49 @@ interface UnblockResult {
// 所有可用平台
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili'];
/**
* 确保对象数据结构完整处理null或undefined的情况
* @param data 需要处理的数据对象
*/
function ensureDataStructure(data: any): any {
// 如果数据本身为空,则返回一个基本结构
if (!data) {
return {
name: '',
artists: [],
album: { name: '' }
};
}
// 确保name字段存在
if (data.name === undefined || data.name === null) {
data.name = '';
}
// 确保artists字段存在且为数组
if (!data.artists || !Array.isArray(data.artists)) {
data.artists = data.ar && Array.isArray(data.ar) ? data.ar : [];
}
// 确保artists中的每个元素都有name属性
if (data.artists.length > 0) {
data.artists = data.artists.map(artist => {
return artist ? { name: artist.name || '' } : { name: '' };
});
}
// 确保album对象存在并有name属性
if (!data.album || typeof data.album !== 'object') {
data.album = data.al && typeof data.al === 'object' ? data.al : { name: '' };
}
if (!data.album.name) {
data.album.name = '';
}
return data;
}
/**
* 音乐解析函数
* @param id 歌曲ID
@@ -46,16 +89,18 @@ const unblockMusic = async (
retryCount = 1,
enabledPlatforms?: Platform[]
): Promise<UnblockResult> => {
// 过滤 enabledPlatforms确保只包含 ALL_PLATFORMS 中存在的平台
const filteredPlatforms = enabledPlatforms
? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform))
: ALL_PLATFORMS;
songData.album = songData.album || songData.al;
songData.artists = songData.artists || songData.ar;
// 处理歌曲数据,确保数据结构完整
const processedSongData = ensureDataStructure(songData);
const retry = async (attempt: number): Promise<UnblockResult> => {
try {
const data = await match(parseInt(String(id), 10), filteredPlatforms, songData);
const data = await match(parseInt(String(id), 10), filteredPlatforms, processedSongData);
const result: UnblockResult = {
data: {
data,

View File

@@ -82,6 +82,66 @@ export const getMusicLrc = async (id: number) => {
}
};
/**
* 从Bilibili获取音频URL
* @param data 歌曲数据
* @returns 解析结果
*/
const getBilibiliAudio = async (data: SongResult) => {
const songName = data?.name || '';
const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
console.log('开始搜索bilibili音频:', searchQuery);
const url = await searchAndGetBilibiliAudioUrl(searchQuery);
return {
data: {
code: 200,
message: 'success',
data: { url }
}
};
};
/**
* 从GD音乐台获取音频URL
* @param id 歌曲ID
* @param data 歌曲数据
* @returns 解析结果失败时返回null
*/
const getGDMusicAudio = async (id: number, data: SongResult) => {
try {
const gdResult = await parseFromGDMusic(id, data, '999');
if (gdResult) {
return gdResult;
}
} catch (error) {
console.error('GD音乐台解析失败:', error);
}
return null;
};
/**
* 使用unblockMusic解析音频URL
* @param id 歌曲ID
* @param data 歌曲数据
* @param sources 音源列表
* @returns 解析结果
*/
const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => {
const filteredSources = sources.filter(source => source !== 'gdmusic');
console.log(`使用unblockMusic解析音源:`, filteredSources);
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
};
/**
* 获取解析后的音乐URL
* @param id 歌曲ID
* @param data 歌曲数据
* @returns 解析结果
*/
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
const settingStore = useSettingsStore();
@@ -90,65 +150,51 @@ export const getParsingMusicUrl = async (id: number, data: SongResult) => {
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
}
// 获取音源设置,优先使用歌曲自定义音源
// 1. 确定使用的音源列表(自定义或全局)
const songId = String(id);
const savedSource = localStorage.getItem(`song_source_${songId}`);
let enabledSources: any[] = [];
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
let musicSources: any[] = [];
// 如果有歌曲自定义音源,使用自定义音源
if (savedSource) {
try {
enabledSources = JSON.parse(savedSource);
console.log(`使用歌曲 ${id} 自定义音源:`, enabledSources);
if(enabledSources.includes('bilibili')){
// 构建搜索关键词,依次判断歌曲名称、歌手名称和专辑名称是否存在
const songName = data?.name || '';
const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
const name = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
console.log('开始搜索bilibili音频', name);
return {
data: {
code: 200,
message: 'success',
data: {
url: await searchAndGetBilibiliAudioUrl(name)
}
}
}
try {
if (savedSourceStr) {
// 使用自定义音源
musicSources = JSON.parse(savedSourceStr);
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
} else {
// 使用全局音源设置
musicSources = settingStore.setData.enabledMusicSources || [];
console.log(`使用全局音源设置:`, musicSources);
if (isElectron && musicSources.length > 0) {
return getUnblockMusicAudio(id, data, musicSources);
}
} catch (e) {
console.error('e',e)
console.error('解析自定义音源失败, 使用全局设置', e);
enabledSources = settingStore.setData.enabledMusicSources || [];
}
} else {
// 没有自定义音源,使用全局音源设置
enabledSources = settingStore.setData.enabledMusicSources || [];
} catch (e) {
console.error('解析音源设置失败,使用全局设置', e);
musicSources = settingStore.setData.enabledMusicSources || [];
}
// 检查是否选择了GD音乐台解析
// 2. 按优先级解析
if (enabledSources.includes('gdmusic')) {
// 获取音质设置并转换为GD音乐台格式
try {
const gdResult = await parseFromGDMusic(id, data, '999');
if (gdResult) {
return gdResult;
}
} catch (error) {
console.error('GD音乐台解析失败:', error);
}
console.log('GD音乐台所有音源均解析失败尝试使用unblockMusic');
// 2.1 Bilibili解析(优先级最高)
if (musicSources.includes('bilibili')) {
return await getBilibiliAudio(data);
}
// 如果GD音乐台解析失败或者未启用尝试使用unblockMusic
if (isElectron) {
const filteredSources = enabledSources.filter(source => source !== 'gdmusic');
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
// 2.2 GD音乐台解析
if (musicSources.includes('gdmusic')) {
const gdResult = await getGDMusicAudio(id, data);
if (gdResult) return gdResult;
// GD解析失败继续下一步
console.log('GD音乐台解析失败尝试使用其他音源');
}
console.log('musicSources',musicSources)
// 2.3 使用unblockMusic解析其他音源
if (isElectron && musicSources.length > 0) {
return getUnblockMusicAudio(id, data, musicSources);
}
// 3. 后备方案使用API请求
console.log('无可用音源或不在Electron环境中使用API请求');
return requestMusic.get<any>('/music', { params: { id } });
};

View File

@@ -5,6 +5,7 @@
placement="right"
@update:show="$emit('update:modelValue', $event)"
:unstable-show-mask="false"
:show-mask="false"
>
<n-drawer-content :title="t('comp.playlistDrawer.title')" class="mac-style-drawer">
<n-scrollbar class="h-full">

View File

@@ -68,11 +68,11 @@
</span>
</n-ellipsis>
</div>
<mini-play-bar
<simple-play-bar
v-if="!config.hideMiniPlayBar"
class="mt-4"
:pure-mode-enabled="config.pureModeEnabled"
component
:isDark=" textColors.theme === 'dark'"
/>
</div>
</div>
@@ -96,7 +96,6 @@
@mouseleave="mouseLeaveLayout"
>
<!-- 歌曲信息 -->
<div ref="lrcContainer" class="music-lrc-container">
<div
v-if="config.hideCover"
@@ -153,7 +152,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import LyricSettings from '@/components/lyric/LyricSettings.vue';
import MiniPlayBar from '@/components/player/MiniPlayBar.vue';
import SimplePlayBar from '@/components/player/SimplePlayBar.vue';
import LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue';
import {
artistList,

View File

@@ -343,7 +343,12 @@ const playMusicEvent = async () => {
}
};
const musicFullVisible = ref(false);
const musicFullVisible = computed({
get: () => playerStore.musicFull,
set: (value) => {
playerStore.setMusicFull(value);
}
})
// 设置musicFull
const setMusicFull = () => {

View File

@@ -52,12 +52,21 @@
<div v-if="playMusic.source === 'bilibili'" class="text-red-500 text-sm">
{{ t('player.reparse.bilibiliNotSupported') }}
</div>
<!-- 清除自定义音源 -->
<div class="text-red-500 text-sm flex items-center bg-light-200 dark:bg-dark-200 rounded-lg p-2 cursor-pointer" @click="clearCustomSource">
<div class="flex items-center justify-center w-6 h-6 mr-3 text-lg">
<i class="ri-close-circle-line"></i>
</div>
<div>
{{ t('player.reparse.clear') }}
</div>
</div>
</div>
</n-popover>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMessage } from 'naive-ui';
import { playMusic } from '@/hooks/MusicHook';
@@ -76,11 +85,7 @@ const currentReparsingSource = ref<Platform | null>(null);
// 实际存储选中音源的值
const selectedSourcesValue = ref<Platform[]>([]);
// 判断当前歌曲是否有自定义解析记录
const isReparse = computed(() => {
const songId = String(playMusic.value.id);
return localStorage.getItem(`song_source_${songId}`) !== null;
});
const isReparse = ref(localStorage.getItem(`song_source_${String(playMusic.value.id)}`) !== null);
// 可选音源列表
const musicSourceOptions = ref([
@@ -127,6 +132,13 @@ const initSelectedSources = () => {
}
};
// 清除自定义音源
const clearCustomSource = () => {
localStorage.removeItem(`song_source_${String(playMusic.value.id)}`);
selectedSourcesValue.value = [];
isReparse.value = false;
};
// 直接重新解析当前歌曲
const directReparseMusic = async (source: Platform) => {
if (isReparsing.value || playMusic.value.source === 'bilibili') {

View File

@@ -0,0 +1,509 @@
<template>
<div class="play-bar" :class="{ 'dark-theme': isDarkMode }" ref="playBarRef">
<div class="container">
<!-- 顶部进度条和时间 -->
<div class="top-section">
<!-- 进度条 -->
<div class="progress-bar" @click="handleProgressClick">
<div class="progress-track"></div>
<div class="progress-fill" :style="{ width: `${(nowTime / allTime) * 100}%` }"></div>
</div>
<!-- 时间显示 -->
<div class="time-display">
<span class="current-time">{{ formatTime(nowTime) }}</span>
<span class="total-time">{{ formatTime(allTime) }}</span>
</div>
</div>
<!-- 主控制区域 -->
<div class="controls-section">
<div class="left-controls">
<button class="control-btn small-btn" @click="togglePlayMode">
<i class="iconfont" :class="playModeIcon"></i>
</button>
</div>
<div class="center-controls">
<!-- 上一首 -->
<button class="control-btn" @click="handlePrev">
<i class="iconfont icon-prev"></i>
</button>
<!-- 播放/暂停 -->
<button class="control-btn play-btn" @click="playMusicEvent">
<i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i>
</button>
<!-- 下一首 -->
<button class="control-btn" @click="handleNext">
<i class="iconfont icon-next"></i>
</button>
</div>
<div class="right-controls">
<!-- 播放列表按钮 -->
<button class="control-btn small-btn" @click="openPlayListDrawer">
<i class="iconfont icon-list"></i>
</button>
</div>
</div>
<!-- 底部控制区域 -->
<div class="bottom-section">
<div class="spacer"></div>
<!-- 音量控制 -->
<div class="volume-control">
<i class="iconfont" :class="getVolumeIcon" @click="mute"></i>
<div class="volume-slider">
<n-slider
v-model:value="volumeSlider"
:step="1"
:tooltip="false"
@wheel.prevent="handleVolumeWheel"
></n-slider>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted, watch } from 'vue';
import { secondToMinute } from '@/utils';
import { allTime, nowTime, playMusic } from '@/hooks/MusicHook';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
const props = withDefaults(defineProps<{
isDark: boolean;
}>(), {
isDark: false
});
const playerStore = usePlayerStore();
const settingsStore = useSettingsStore();
const playBarRef = ref<HTMLElement | null>(null);
// 播放状态
const play = computed(() => playerStore.isPlay);
// 播放模式
const playMode = computed(() => playerStore.playMode);
const playModeIcon = computed(() => {
switch (playMode.value) {
case 0:
return 'ri-repeat-2-line';
case 1:
return 'ri-repeat-one-line';
case 2:
return 'ri-shuffle-line';
default:
return 'ri-repeat-2-line';
}
});
// 切换播放模式
const togglePlayMode = () => {
playerStore.togglePlayMode();
};
// 音量控制
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();
}
};
// 进度条控制
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 formatTime = (seconds: number) => {
return secondToMinute(seconds);
};
// 打开播放列表抽屉
const openPlayListDrawer = () => {
playerStore.setPlayListDrawerVisible(true);
};
// 深色模式
const isDarkMode = computed(() => settingsStore.theme === 'dark' || props.isDark);
// 主题颜色应用函数
const applyThemeColor = (colorValue: string) => {
if (!colorValue || !playBarRef.value) return;
console.log('应用主题色:', colorValue);
const playBarElement = playBarRef.value;
// 解析RGB值
const rgbMatch = colorValue.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (rgbMatch) {
const [_, r, g, b] = rgbMatch.map(Number);
// 计算颜色亮度 (0-255)
// 使用加权平均值公式: 0.299*R + 0.587*G + 0.114*B
const brightness = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
console.log(`主题色亮度: ${brightness}/255`);
// 设置主色
playBarElement.style.setProperty('--fill-color', colorValue);
// 亮度自适应处理
if (brightness > 200) { // 非常亮的颜色
// 深化主色以增加对比度
const darkenedColor = `rgb(${Math.max(0, r - 60)}, ${Math.max(0, g - 60)}, ${Math.max(0, b - 60)})`;
playBarElement.style.setProperty('--fill-color-alt', darkenedColor);
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.5)`); // 提高透明度
playBarElement.style.setProperty('--text-on-fill', '#000000'); // 亮色背景上用黑色文字
playBarElement.style.setProperty('--high-contrast-color', '#000000'); // 高对比度颜色
playBarElement.classList.add('light-theme-color');
playBarElement.classList.remove('dark-theme-color');
} else if (brightness < 50) { // 非常暗的颜色
// 提亮主色以增加可见性
const lightenedColor = `rgb(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 60)})`;
playBarElement.style.setProperty('--fill-color-alt', lightenedColor);
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.7)`); // 提高透明度
playBarElement.style.setProperty('--text-on-fill', '#ffffff'); // 暗色背景上用白色文字
playBarElement.style.setProperty('--high-contrast-color', '#ffffff'); // 高对比度颜色
playBarElement.classList.add('dark-theme-color');
playBarElement.classList.remove('light-theme-color');
} else {
// 计算辅助色和高亮色
// 普通亮度颜色,正常处理
playBarElement.style.setProperty('--fill-color-alt', colorValue); // 保持一致
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.25)`);
// 根据亮度决定文本颜色
const textColor = brightness > 125 ? '#000000' : '#ffffff';
playBarElement.style.setProperty('--text-on-fill', textColor);
playBarElement.style.setProperty('--high-contrast-color', textColor);
playBarElement.classList.remove('light-theme-color');
playBarElement.classList.remove('dark-theme-color');
}
// 设置亮色(用于高亮效果)
const lightenedColor = `rgb(${Math.min(255, r + 40)}, ${Math.min(255, g + 40)}, ${Math.min(255, b + 40)})`;
playBarElement.style.setProperty('--fill-color-light', lightenedColor);
} else {
// 无法解析RGB值时的默认设置
playBarElement.style.setProperty('--fill-color', colorValue);
playBarElement.style.setProperty('--fill-color-transparent', `${colorValue}40`);
playBarElement.style.setProperty('--fill-color-light', `${colorValue}80`);
playBarElement.style.setProperty('--fill-color-alt', colorValue);
playBarElement.style.setProperty('--text-on-fill', '#ffffff');
playBarElement.style.setProperty('--high-contrast-color', '#ffffff');
}
};
// 监听主题色变化
watch(() => playerStore.playMusic.primaryColor, (newVal) => {
if (newVal) {
applyThemeColor(newVal);
}
});
onMounted(() => {
if (playerStore.playMusic?.primaryColor) {
setTimeout(() => {
applyThemeColor(playerStore.playMusic.primaryColor as string);
}, 50);
}
});
</script>
<style lang="scss" scoped>
.play-bar {
@apply w-full;
border-radius: 12px;
transition: all 0.3s ease;
/* 默认变量 */
--text-on-fill: #ffffff;
--high-contrast-color: #ffffff;
&.dark-theme {
--text-color: #333333;
--muted-color: rgba(0, 0, 0, 0.6);
--track-color: rgba(0, 0, 0, 0.2);
--track-color-hover: rgba(0, 0, 0, 0.4);
--fill-color: #1ed760;
--fill-color-alt: #1ed760;
--fill-color-transparent: rgba(30, 215, 96, 0.25);
--fill-color-light: rgba(30, 215, 96, 0.5);
--button-bg: rgba(0, 0, 0, 0.1);
--button-hover: rgba(0, 0, 0, 0.2);
}
&:not(.dark-theme) {
--text-color: #f1f1f1;
--muted-color: rgba(255, 255, 255, 0.6);
--track-color: rgba(255, 255, 255, 0.1);
--track-color-hover: rgba(255, 255, 255, 0.2);
--fill-color: #73e49a;
--fill-color-alt: #73e49a;
--fill-color-transparent: rgba(115, 228, 154, 0.25);
--fill-color-light: rgba(115, 228, 154, 0.5);
--button-bg: rgba(255, 255, 255, 0.05);
--button-hover: rgba(255, 255, 255, 0.1);
}
/* 极亮主题色适配 */
&.light-theme-color {
.progress-fill {
box-shadow: 0 0 8px var(--fill-color-transparent), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.control-btn.play-btn {
box-shadow: 0 3px 8px var(--fill-color-transparent), 0 1px 2px rgba(0, 0, 0, 0.3);
color: var(--text-on-fill);
}
.volume-control .iconfont:hover {
color: var(--fill-color-alt);
}
}
/* 极暗主题色适配 */
&.dark-theme-color {
.progress-fill {
box-shadow: 0 0 10px var(--fill-color-transparent), inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.control-btn.play-btn {
box-shadow: 0 3px 12px var(--fill-color-transparent), 0 0 0 1px rgba(255, 255, 255, 0.2);
.iconfont {
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
}
.volume-control .iconfont:hover {
color: var(--fill-color-light);
}
}
}
.container {
@apply flex flex-col;
}
.top-section {
@apply mb-3;
.progress-bar {
@apply relative cursor-pointer h-2 mb-2 w-full;
.progress-track {
@apply absolute inset-0 rounded-full transition-all duration-150;
background-color: var(--track-color);
}
.progress-fill {
@apply absolute top-0 left-0 h-full rounded-full transition-all duration-150;
background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));
box-shadow: 0 0 8px var(--fill-color-transparent);
}
&:hover {
.progress-track{
background-color: var(--track-color-hover);
}
.progress-track, .progress-fill {
@apply h-full;
}
.progress-fill {
box-shadow: 0 0 12px var(--fill-color-transparent);
}
}
}
.time-display {
@apply flex justify-between text-base;
color: var(--muted-color);
.time-separator {
@apply mx-1;
}
.current-time {
opacity: 0.8;
transition: opacity 0.3s ease;
&:hover {
opacity: 1;
}
}
}
}
.controls-section {
@apply flex items-center justify-between mb-4;
.left-controls, .right-controls {
@apply flex items-center;
}
.center-controls {
@apply flex items-center justify-center space-x-6;
}
}
.bottom-section {
@apply flex items-center justify-between mt-2;
}
.control-btn {
@apply flex items-center justify-center rounded-full outline-none border-0 transition-all duration-200;
color: var(--text-color);
background: transparent;
width: 32px;
height: 32px;
cursor: pointer;
&:hover {
background-color: var(--button-bg);
transform: scale(1.05);
}
&:active {
background-color: var(--button-hover);
transform: scale(0.95);
}
&.play-btn {
background: linear-gradient(145deg, var(--fill-color), var(--fill-color-alt));
color: var(--text-on-fill);
width: 46px;
height: 46px;
box-shadow: 0 3px 8px var(--fill-color-transparent);
&:hover {
box-shadow: 0 4px 12px var(--fill-color-transparent);
}
.iconfont {
font-size: 1.25rem;
}
}
&.small-btn {
@apply text-2xl;
width: 28px;
height: 28px;
}
.iconfont {
@apply text-2xl;
}
}
.volume-control {
@apply flex items-center space-x-2;
color: var(--text-color);
.iconfont {
@apply cursor-pointer text-base;
transition: transform 0.2s ease, color 0.2s ease;
&:hover {
transform: scale(1.1);
color: var(--fill-color);
}
}
.volume-slider {
@apply w-24;
:deep(.n-slider) {
--n-rail-height: 3px;
--n-fill-color: var(--fill-color);
--n-rail-color: var(--track-color);
--n-handle-size: 12px;
.n-slider-rail {
@apply rounded-full;
}
.n-slider-rail__fill {
background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));
box-shadow: 0 0 6px var(--fill-color-transparent);
}
.n-slider-handle {
@apply opacity-0 transition-opacity duration-200;
background: white;
box-shadow: 0 0 6px var(--fill-color-transparent), 0 0 0 1px var(--high-contrast-color);
border: 2px solid var(--fill-color);
}
&:hover .n-slider-handle {
@apply opacity-100;
}
}
}
}
.spacer {
flex: 1;
}
.like-active {
color: var(--fill-color);
text-shadow: 0 0 8px var(--fill-color-transparent);
}
</style>

View File

@@ -67,10 +67,10 @@ const visible = ref(props.show);
const selectedSources = ref<Platform[]>(props.sources);
const musicSourceOptions = ref([
{ label: 'MiGu音乐', value: 'migu' },
{ label: '酷狗音乐', value: 'kugou' },
{ label: 'MG', value: 'migu' },
{ label: 'KG', value: 'kugou' },
{ label: 'pyncmd', value: 'pyncmd' },
{ label: 'Bilibili音乐', value: 'bilibili' },
{ label: 'Bilibili', value: 'bilibili' },
{ label: 'GD音乐台', value: 'gdmusic' }
]);

View File

@@ -125,6 +125,8 @@ const currentSongId = ref<number | undefined>();
const openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {
currentSongId.value = songId;
showPlaylistDrawer.value = isOpen;
playerStore.setMusicFull(false);
playerStore.setPlayListDrawerVisible(!isOpen);
};
// 将方法提供给全局

View File

@@ -811,6 +811,40 @@ class AudioService {
console.log('Volume applied (linear):', linearVolume);
}
// 添加方法检查当前音频是否在加载状态
isLoading(): boolean {
if (!this.currentSound) return false;
// 检查Howl对象的内部状态
// 如果状态为1表示已经加载但未完成状态为2表示正在加载
const state = (this.currentSound as any)._state;
// 如果操作锁激活也认为是加载状态
return this.operationLock || (state === 'loading' || state === 1);
}
// 检查音频是否真正在播放
isActuallyPlaying(): boolean {
if (!this.currentSound) return false;
try {
// 综合判断:
// 1. Howler API是否报告正在播放
// 2. 是否不在加载状态
// 3. 确保音频上下文状态正常
const isPlaying = this.currentSound.playing();
const isLoading = this.isLoading();
const contextRunning = Howler.ctx && Howler.ctx.state === 'running';
console.log(`实际播放状态检查: playing=${isPlaying}, loading=${isLoading}, contextRunning=${contextRunning}`);
// 只有在三个条件都满足时才认为是真正在播放
return isPlaying && !isLoading && contextRunning;
} catch (error) {
console.error('检查播放状态出错:', error);
return false;
}
}
}
export const audioService = new AudioService();

View File

@@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useThrottleFn } from '@vueuse/core';
import i18n from '@/../i18n/renderer';
import { getBilibiliAudioUrl } from '@/api/bilibili';
@@ -556,16 +557,95 @@ export const usePlayerStore = defineStore('player', () => {
}
};
// 添加用户意图跟踪变量
const userPlayIntent = ref(true);
let checkPlayTime: NodeJS.Timeout | null = null;
// 添加独立的播放状态检测函数
const checkPlaybackState = (song: SongResult, timeout: number = 4000) => {
if(checkPlayTime) {
clearTimeout(checkPlayTime);
}
const sound = audioService.getCurrentSound();
if (!sound) return;
// 使用audioService的事件系统监听播放状态
// 添加一次性播放事件监听器
const onPlayHandler = () => {
// 播放事件触发,表示成功播放
console.log('播放事件触发,歌曲成功开始播放');
audioService.off('play', onPlayHandler);
audioService.off('playerror', onPlayErrorHandler);
};
// 添加一次性播放错误事件监听器
const onPlayErrorHandler = async () => {
console.log('播放错误事件触发尝试重新获取URL');
audioService.off('play', onPlayHandler);
audioService.off('playerror', onPlayErrorHandler);
// 只有用户仍然希望播放时才重试
if (userPlayIntent.value && play.value) {
// 重置URL并重新播放
playMusic.value.playMusicUrl = undefined;
// 保持播放状态但强制重新获取URL
const refreshedSong = { ...song, isFirstPlay: true };
await handlePlayMusic(refreshedSong, true);
}
};
// 注册事件监听器
audioService.on('play', onPlayHandler);
audioService.on('playerror', onPlayErrorHandler);
// 额外的安全检查:如果指定时间后仍未播放也未触发错误,且用户仍希望播放
checkPlayTime = setTimeout(() => {
// 使用更准确的方法检查是否真正在播放
if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) {
console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放尝试重新获取URL`);
// 移除事件监听器
audioService.off('play', onPlayHandler);
audioService.off('playerror', onPlayErrorHandler);
// 重置URL并重新播放
playMusic.value.playMusicUrl = undefined;
// 保持播放状态强制重新获取URL
(async () => {
const refreshedSong = { ...song, isFirstPlay: true };
await handlePlayMusic(refreshedSong, true);
})();
}
}, timeout);
};
const setPlay = async (song: SongResult) => {
try {
// 如果是当前正在播放的音乐,则切换播放/暂停状态
if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) {
if (play.value) {
// 检查URL是否已过期
if (song.expiredAt && song.expiredAt < Date.now()) {
console.info(`歌曲URL已过期重新获取: ${song.name}`);
song.playMusicUrl = undefined;
// 重置过期时间,以便重新获取
song.expiredAt = undefined;
}
// 如果是当前正在播放的音乐,则切换播放/暂停状态
if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) {
if (play.value) {
setPlayMusic(false);
audioService.getCurrentSound()?.pause();
// 设置用户意图为暂停
userPlayIntent.value = false;
} else {
setPlayMusic(true);
audioService.getCurrentSound()?.play();
// 设置用户意图为播放
userPlayIntent.value = true;
const sound = audioService.getCurrentSound();
if (sound) {
sound.play();
// 使用独立的播放状态检测函数
checkPlaybackState(playMusic.value);
}
}
return;
}
@@ -600,10 +680,14 @@ export const usePlayerStore = defineStore('player', () => {
const setPlayMusic = async (value: boolean | SongResult) => {
if (typeof value === 'boolean') {
setIsPlay(value);
// 记录用户的播放意图
userPlayIntent.value = value;
} else {
await handlePlayMusic(value);
play.value = true;
isPlay.value = true;
// 设置为播放意图
userPlayIntent.value = true;
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
}
@@ -803,8 +887,7 @@ export const usePlayerStore = defineStore('player', () => {
}
};
// 修改nextPlay方法改进播放逻辑
const nextPlay = async () => {
const _nextPlay = async () => {
try {
@@ -927,9 +1010,10 @@ export const usePlayerStore = defineStore('player', () => {
}
};
// 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进
const prevPlay = async () => {
// 节流
const nextPlay = useThrottleFn(_nextPlay, 500);
const _prevPlay = async () => {
try {
@@ -1010,6 +1094,9 @@ export const usePlayerStore = defineStore('player', () => {
}
};
// 节流
const prevPlay = useThrottleFn(_prevPlay, 500);
const togglePlayMode = () => {
playMode.value = (playMode.value + 1) % 3;
localStorage.setItem('playMode', JSON.stringify(playMode.value));
@@ -1185,6 +1272,12 @@ export const usePlayerStore = defineStore('player', () => {
// 播放新音频,传递是否应该播放的状态
console.log('调用audioService.play播放状态:', shouldPlay);
const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay, initialPosition || 0);
// 添加播放状态检测(仅当需要播放时)
if (shouldPlay) {
checkPlaybackState(playMusic.value);
}
// 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器
window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }));
@@ -1215,15 +1308,18 @@ export const usePlayerStore = defineStore('player', () => {
// 延迟较长时间,确保锁已完全释放
setTimeout(() => {
// 直接重试当前歌曲,而不是切换到下一首
playAudio().catch(e => {
console.error('重试播放失败,切换到下一首:', e);
// 只有再次失败切换到下一首
if (playList.value.length > 1) {
nextPlay();
}
});
// 如果用户仍希望播放
if (userPlayIntent.value && play.value) {
// 直接重试当前歌曲,而不是切换到下一首
playAudio().catch(e => {
console.error('重试播放失败切换到下一首:', e);
// 只有再次失败才切换到下一首
if (playList.value.length > 1) {
nextPlay();
}
});
}
}, 1000);
} else {
// 其他错误,切换到下一首
@@ -1253,7 +1349,7 @@ export const usePlayerStore = defineStore('player', () => {
return false;
}
// 保存用户选择的音源
// 保存用户选择的音源作为数组传递确保unblockMusic可以使用
const songId = String(currentSong.id);
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
@@ -1267,11 +1363,17 @@ export const usePlayerStore = defineStore('player', () => {
const numericId = typeof currentSong.id === 'string'
? parseInt(currentSong.id, 10)
: currentSong.id;
console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`);
const res = await getParsingMusicUrl(numericId, cloneDeep(currentSong));
// 克隆一份歌曲数据,防止修改原始数据
const songData = cloneDeep(currentSong);
const res = await getParsingMusicUrl(numericId, songData);
if (res && res.data && res.data.data && res.data.data.url) {
// 更新URL
const newUrl = res.data.data.url;
console.log(`解析成功获取新URL: ${newUrl.substring(0, 50)}...`);
// 使用新URL更新播放
const updatedMusic = {
@@ -1286,6 +1388,7 @@ export const usePlayerStore = defineStore('player', () => {
return true;
} else {
console.warn(`使用音源 ${sourcePlatform} 解析失败`);
return false;
}
} catch (error) {
@@ -1307,6 +1410,8 @@ export const usePlayerStore = defineStore('player', () => {
currentSound.pause();
}
setPlayMusic(false);
// 明确设置用户意图为暂停
userPlayIntent.value = false;
} catch (error) {
console.error('暂停播放失败:', error);
}
@@ -1345,8 +1450,8 @@ export const usePlayerStore = defineStore('player', () => {
clearPlayAll,
setPlay,
setIsPlay,
nextPlay,
prevPlay,
nextPlay: nextPlay as unknown as typeof _nextPlay,
prevPlay: prevPlay as unknown as typeof _prevPlay,
setPlayMusic,
setMusicFull,
setPlayList,