mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-15 07:20:49 +08:00
✨ feat: 添加B站音频URL获取功能,优化播放器逻辑,删除不再使用的BilibiliPlayer和MusicBar组件
This commit is contained in:
@@ -106,7 +106,6 @@ if (!isSingleInstance) {
|
||||
|
||||
// 监听语言切换
|
||||
ipcMain.on('change-language', (_, locale: Language) => {
|
||||
console.log('locale',locale)
|
||||
// 更新主进程的语言设置
|
||||
i18n.global.locale = locale;
|
||||
// 更新托盘菜单
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { is } from '@electron-toolkit/utils';
|
||||
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
|
||||
import { app, BrowserWindow, globalShortcut, ipcMain, session, shell } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { join } from 'path';
|
||||
|
||||
@@ -112,6 +112,11 @@ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
|
||||
// 注册快捷键 打开开发者工具
|
||||
globalShortcut.register('CommandOrControl+Shift+I', () => {
|
||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
});
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
|
||||
@@ -126,3 +126,25 @@ export const getBilibiliProxyUrl = (url: string) => {
|
||||
const AUrl = url.startsWith('http') ? url : `https:${url}`;
|
||||
return `${import.meta.env.VITE_API}/bilibili/stream-proxy?url=${encodeURIComponent(AUrl)}`;
|
||||
};
|
||||
|
||||
export const getBilibiliAudioUrl = async (bvid: string, cid: number): Promise<string> => {
|
||||
console.log('获取B站音频URL', { bvid, cid });
|
||||
try {
|
||||
const res = await getBilibiliPlayUrl(bvid, cid);
|
||||
const playUrlData = res.data;
|
||||
let url = '';
|
||||
|
||||
if (playUrlData.dash && playUrlData.dash.audio && playUrlData.dash.audio.length > 0) {
|
||||
url = playUrlData.dash.audio[0].baseUrl;
|
||||
} else if (playUrlData.durl && playUrlData.durl.length > 0) {
|
||||
url = playUrlData.durl[0].url;
|
||||
} else {
|
||||
throw new Error('未找到可用的音频地址');
|
||||
}
|
||||
|
||||
return getBilibiliProxyUrl(url);
|
||||
} catch (error) {
|
||||
console.error('获取B站音频URL失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="music-bar-container"
|
||||
:class="{ playing: playerStore.isPlay && playerStore.playMusicUrl }"
|
||||
>
|
||||
<div class="music-bar-left">
|
||||
<div
|
||||
class="music-cover"
|
||||
:class="{ loading: playerStore.currentSong?.playLoading }"
|
||||
@click="openDetailPlayer"
|
||||
>
|
||||
<n-image
|
||||
v-if="playerStore.currentSong && playerStore.currentSong.picUrl"
|
||||
class="cover-image"
|
||||
:src="getSourcePic(playerStore.currentSong)"
|
||||
preview-disabled
|
||||
:object-fit="'cover'"
|
||||
/>
|
||||
<n-spin v-if="playerStore.currentSong?.playLoading" size="small" />
|
||||
</div>
|
||||
<div class="music-info">
|
||||
<div class="music-title ellipsis-text">
|
||||
{{ playerStore.currentSong ? playerStore.currentSong.name : '' }}
|
||||
</div>
|
||||
<div class="music-artist ellipsis-text">
|
||||
{{ getArtistName(playerStore.currentSong) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-bar-center">
|
||||
<div class="player-controls">
|
||||
<n-button quaternary circle class="control-button prev-button" @click="handlePrevClick">
|
||||
<template #icon>
|
||||
<i class="ri-skip-back-fill"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
quaternary
|
||||
circle
|
||||
class="control-button play-button"
|
||||
:disabled="!playerStore.currentSong.id"
|
||||
@click="handlePlayClick"
|
||||
>
|
||||
<template #icon>
|
||||
<i :class="playerStore.isPlay ? 'ri-pause-fill' : 'ri-play-fill'"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button quaternary circle class="control-button next-button" @click="handleNextClick">
|
||||
<template #icon>
|
||||
<i class="ri-skip-forward-fill"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<div class="time-text">{{ formatTime(currentTime) }}</div>
|
||||
<n-slider
|
||||
class="progress-slider"
|
||||
:value="playerProgress"
|
||||
:step="0.1"
|
||||
:max="duration"
|
||||
:tooltip="false"
|
||||
@update:value="handleProgressUpdate"
|
||||
/>
|
||||
<div class="time-text">{{ formatTime(duration) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-bar-right">
|
||||
<n-button quaternary circle class="control-button mode-button" @click="togglePlayMode">
|
||||
<template #icon>
|
||||
<i v-if="playerStore.playMode === 0" class="ri-repeat-2-line" />
|
||||
<i v-else-if="playerStore.playMode === 1" class="ri-repeat-one-line" />
|
||||
<i v-else-if="playerStore.playMode === 2" class="ri-shuffle-line" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button quaternary circle class="control-button list-button" @click="togglePlayList">
|
||||
<template #icon>
|
||||
<i class="ri-play-list-line"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { getBilibiliProxyUrl } from '@/api/bilibili';
|
||||
import { usePlayerStore } from '@/store/modules/player';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
const router = useRouter();
|
||||
const playerStore = usePlayerStore();
|
||||
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const playerProgress = ref(0);
|
||||
const showPlayList = ref(false);
|
||||
|
||||
let audioPlayer: HTMLAudioElement | null = null;
|
||||
|
||||
// 计算播放进度
|
||||
const progress = computed(() => {
|
||||
if (duration.value === 0) return 0;
|
||||
return (currentTime.value / duration.value) * 100;
|
||||
});
|
||||
|
||||
// 监听播放状态
|
||||
watch(
|
||||
() => playerStore.isPlay,
|
||||
(isPlay) => {
|
||||
if (!audioPlayer) return;
|
||||
if (isPlay) {
|
||||
audioPlayer.play().catch((error) => {
|
||||
console.error('播放失败:', error);
|
||||
playerStore.setIsPlay(false);
|
||||
});
|
||||
} else {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听播放URL变化
|
||||
watch(
|
||||
() => playerStore.playMusicUrl,
|
||||
(newUrl) => {
|
||||
if (!audioPlayer) return;
|
||||
if (newUrl) {
|
||||
audioPlayer.src = newUrl;
|
||||
if (playerStore.play) {
|
||||
audioPlayer.play().catch((error) => {
|
||||
console.error('播放失败:', error);
|
||||
playerStore.setIsPlay(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 初始化播放器
|
||||
onMounted(() => {
|
||||
audioPlayer = new Audio();
|
||||
|
||||
// 设置初始音频
|
||||
if (playerStore.playMusicUrl) {
|
||||
audioPlayer.src = playerStore.playMusicUrl;
|
||||
|
||||
// 如果应该自动播放
|
||||
if (playerStore.play) {
|
||||
audioPlayer.play().catch((error) => {
|
||||
console.error('播放失败:', error);
|
||||
playerStore.setIsPlay(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加事件监听
|
||||
audioPlayer.addEventListener('timeupdate', handleTimeUpdate);
|
||||
audioPlayer.addEventListener('loadeddata', handleLoadedData);
|
||||
audioPlayer.addEventListener('ended', handleEnded);
|
||||
audioPlayer.addEventListener('pause', () => playerStore.setIsPlay(false));
|
||||
audioPlayer.addEventListener('play', () => playerStore.setIsPlay(true));
|
||||
audioPlayer.addEventListener('error', handleError);
|
||||
});
|
||||
|
||||
// 处理播放时间更新
|
||||
const handleTimeUpdate = () => {
|
||||
if (!audioPlayer) return;
|
||||
currentTime.value = audioPlayer.currentTime;
|
||||
playerProgress.value = currentTime.value;
|
||||
|
||||
// 保存播放进度到localStorage
|
||||
const playProgress = {
|
||||
songId: playerStore.currentSong.id,
|
||||
progress: currentTime.value
|
||||
};
|
||||
localStorage.setItem('playProgress', JSON.stringify(playProgress));
|
||||
};
|
||||
|
||||
// 处理音频加载完成
|
||||
const handleLoadedData = () => {
|
||||
if (!audioPlayer) return;
|
||||
duration.value = audioPlayer.duration;
|
||||
|
||||
// 如果有保存的播放进度,恢复到对应位置
|
||||
if (playerStore.savedPlayProgress !== undefined) {
|
||||
audioPlayer.currentTime = playerStore.savedPlayProgress;
|
||||
playerStore.savedPlayProgress = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理播放结束
|
||||
const handleEnded = () => {
|
||||
// 根据播放模式决定下一步操作
|
||||
if (playerStore.playMode === 1) {
|
||||
// 单曲循环
|
||||
if (audioPlayer) {
|
||||
audioPlayer.currentTime = 0;
|
||||
audioPlayer.play().catch((error) => {
|
||||
console.error('重新播放失败:', error);
|
||||
playerStore.setIsPlay(false);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 列表循环或随机播放
|
||||
playerStore.nextPlay();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理播放错误
|
||||
const handleError = (e: Event) => {
|
||||
console.error('音频播放出错:', e);
|
||||
if (audioPlayer?.error) {
|
||||
console.error('错误码:', audioPlayer.error.code);
|
||||
|
||||
// 如果是B站音频可能链接过期,尝试重新获取
|
||||
if (playerStore.currentSong.source === 'bilibili' && playerStore.currentSong.bilibiliData) {
|
||||
console.log('B站音频播放错误,可能链接过期');
|
||||
// 这里可以添加重新获取B站音频链接的逻辑
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理播放/暂停按钮点击
|
||||
const handlePlayClick = () => {
|
||||
if (!playerStore.currentSong.id) return;
|
||||
playerStore.setPlayMusic(!playerStore.isPlay);
|
||||
};
|
||||
|
||||
// 处理上一曲按钮点击
|
||||
const handlePrevClick = () => {
|
||||
playerStore.prevPlay();
|
||||
};
|
||||
|
||||
// 处理下一曲按钮点击
|
||||
const handleNextClick = () => {
|
||||
playerStore.nextPlay();
|
||||
};
|
||||
|
||||
// 处理进度条更新
|
||||
const handleProgressUpdate = (value: number) => {
|
||||
if (!audioPlayer) return;
|
||||
audioPlayer.currentTime = value;
|
||||
currentTime.value = value;
|
||||
};
|
||||
|
||||
// 切换播放模式
|
||||
const togglePlayMode = () => {
|
||||
playerStore.togglePlayMode();
|
||||
};
|
||||
|
||||
// 切换播放列表显示
|
||||
const togglePlayList = () => {
|
||||
showPlayList.value = !showPlayList.value;
|
||||
// 这里可以添加显示播放列表的逻辑
|
||||
};
|
||||
|
||||
// 打开详情播放页
|
||||
const openDetailPlayer = () => {
|
||||
if (!playerStore.currentSong.id) return;
|
||||
playerStore.setMusicFull(true);
|
||||
|
||||
// 根据来源打开不同的详情页
|
||||
if (playerStore.currentSong.source === 'bilibili') {
|
||||
// 打开B站详情页
|
||||
if (playerStore.currentSong.bilibiliData?.bvid) {
|
||||
// 这里可以跳转到B站详情页或显示B站播放器组件
|
||||
}
|
||||
} else {
|
||||
// 打开网易云音乐详情页
|
||||
router.push({ path: '/lyric' });
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: number) => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 获取艺术家名称
|
||||
const getArtistName = (song?: SongResult) => {
|
||||
if (!song) return '';
|
||||
|
||||
// B站视频
|
||||
if (song.source === 'bilibili' && song.song?.ar?.[0]) {
|
||||
return song.song.ar[0].name || 'B站UP主';
|
||||
}
|
||||
|
||||
// 网易云音乐
|
||||
if (song.song?.artists) {
|
||||
return song.song.artists.map((artist: any) => artist.name).join('/');
|
||||
}
|
||||
|
||||
if (song.song?.ar) {
|
||||
return song.song.ar.map((artist: any) => artist.name).join('/');
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// 根据来源获取封面图
|
||||
const getSourcePic = (song: SongResult) => {
|
||||
if (!song || !song.picUrl) return '';
|
||||
|
||||
// B站视频
|
||||
if (song.source === 'bilibili') {
|
||||
return song.picUrl; // B站封面已经在创建时使用getBilibiliProxyUrl处理过
|
||||
}
|
||||
|
||||
// 网易云音乐
|
||||
return getImgUrl(song.picUrl, '150y150');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.music-bar-container {
|
||||
@apply flex items-center justify-between px-4 h-20 w-full bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700;
|
||||
|
||||
&.playing {
|
||||
.music-cover {
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music-bar-left {
|
||||
@apply flex items-center;
|
||||
width: 25%;
|
||||
|
||||
.music-cover {
|
||||
@apply relative w-12 h-12 rounded-lg overflow-hidden flex items-center justify-center cursor-pointer;
|
||||
|
||||
.cover-image {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
|
||||
.music-info {
|
||||
@apply ml-2 flex flex-col;
|
||||
max-width: calc(100% - 56px);
|
||||
|
||||
.music-title {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
|
||||
.music-artist {
|
||||
@apply text-xs text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music-bar-center {
|
||||
@apply flex flex-col items-center;
|
||||
width: 50%;
|
||||
|
||||
.player-controls {
|
||||
@apply flex items-center justify-center gap-2 mb-2;
|
||||
|
||||
.control-button {
|
||||
@apply text-lg;
|
||||
|
||||
&.play-button {
|
||||
@apply text-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
@apply flex items-center w-full;
|
||||
|
||||
.time-text {
|
||||
@apply text-xs px-2 text-gray-500 dark:text-gray-400 whitespace-nowrap;
|
||||
}
|
||||
|
||||
.progress-slider {
|
||||
@apply flex-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music-bar-right {
|
||||
@apply flex items-center justify-end;
|
||||
width: 25%;
|
||||
|
||||
.control-button {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis-text {
|
||||
@apply whitespace-nowrap overflow-hidden text-ellipsis;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -140,7 +140,7 @@ const dropdownY = ref(0);
|
||||
|
||||
const isDownloading = ref(false);
|
||||
|
||||
const openPlaylistDrawer = inject<(songId: number) => void>('openPlaylistDrawer');
|
||||
const openPlaylistDrawer = inject<(songId: number | string) => void>('openPlaylistDrawer');
|
||||
|
||||
const { navigateToArtist } = useArtist();
|
||||
|
||||
@@ -285,7 +285,7 @@ const downloadMusic = async () => {
|
||||
try {
|
||||
isDownloading.value = true;
|
||||
|
||||
const data = (await getSongUrl(props.item.id, cloneDeep(props.item), true)) as any;
|
||||
const data = (await getSongUrl(props.item.id as number, cloneDeep(props.item), true)) as any;
|
||||
if (!data || !data.url) {
|
||||
throw new Error(t('songItem.message.getUrlFailed'));
|
||||
}
|
||||
@@ -358,6 +358,7 @@ const imageLoad = async () => {
|
||||
|
||||
// 播放音乐 设置音乐详情 打开音乐底栏
|
||||
const playMusicEvent = async (item: SongResult) => {
|
||||
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
||||
if (playMusic.value.id === item.id) {
|
||||
if (play.value) {
|
||||
playerStore.setPlayMusic(false);
|
||||
@@ -368,23 +369,37 @@ const playMusicEvent = async (item: SongResult) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
await playerStore.setPlay(item);
|
||||
playerStore.isPlay = true;
|
||||
emits('play', item);
|
||||
|
||||
try {
|
||||
// 使用store的setPlay方法,该方法已经包含了B站视频URL处理逻辑
|
||||
const result = await playerStore.setPlay(item);
|
||||
if (!result) {
|
||||
throw new Error('播放失败');
|
||||
}
|
||||
playerStore.isPlay = true;
|
||||
emits('play', item);
|
||||
} catch (error) {
|
||||
console.error('播放出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 判断是否已收藏
|
||||
const isFavorite = computed(() => {
|
||||
return playerStore.favoriteList.includes(props.item.id);
|
||||
// 将id转换为number,兼容B站视频ID
|
||||
const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id;
|
||||
return playerStore.favoriteList.includes(numericId);
|
||||
});
|
||||
|
||||
// 切换收藏状态
|
||||
const toggleFavorite = async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
// 将id转换为number,兼容B站视频ID
|
||||
const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id;
|
||||
|
||||
if (isFavorite.value) {
|
||||
playerStore.removeFromFavorite(props.item.id);
|
||||
playerStore.removeFromFavorite(numericId);
|
||||
} else {
|
||||
playerStore.addToFavorite(props.item.id);
|
||||
playerStore.addToFavorite(numericId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createDiscreteApi } from 'naive-ui';
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import i18n from '@/../i18n/renderer';
|
||||
import { getBilibiliAudioUrl } from '@/api/bilibili';
|
||||
import useIndexedDB from '@/hooks/IndexDBHook';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import pinia, { usePlayerStore } from '@/store';
|
||||
@@ -235,6 +236,29 @@ watch(
|
||||
initialPosition = savedProgress.progress;
|
||||
}
|
||||
|
||||
// 对于B站视频,检查URL是否有效
|
||||
if (playMusic.value.source === 'bilibili' && (!newVal || newVal === 'undefined')) {
|
||||
console.log('B站视频URL无效,尝试重新获取');
|
||||
|
||||
// 需要重新获取B站视频URL
|
||||
if (playMusic.value.bilibiliData) {
|
||||
try {
|
||||
const proxyUrl = await getBilibiliAudioUrl(
|
||||
playMusic.value.bilibiliData.bvid,
|
||||
playMusic.value.bilibiliData.cid
|
||||
);
|
||||
|
||||
// 设置URL到播放器状态
|
||||
(playMusic.value as any).playMusicUrl = proxyUrl;
|
||||
playerStore.playMusicUrl = proxyUrl;
|
||||
newVal = proxyUrl;
|
||||
} catch (error) {
|
||||
console.error('获取B站音频URL失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放新音频,传递是否应该播放的状态
|
||||
const newSound = await audioService.play(newVal, playMusic.value, shouldPlay);
|
||||
sound.value = newSound as Howl;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getImageLinearBackground } from '@/utils/linearColor';
|
||||
const musicHistory = useMusicHistory();
|
||||
|
||||
// 获取歌曲url
|
||||
export const getSongUrl = async (id: number, songData: any, isDownloaded: boolean = false) => {
|
||||
export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean = false) => {
|
||||
const { data } = await getMusicUrl(id, isDownloaded);
|
||||
let url = '';
|
||||
let songDetail = null;
|
||||
@@ -247,7 +247,7 @@ export const useMusicListHook = () => {
|
||||
};
|
||||
|
||||
// 异步加载歌词的方法
|
||||
const loadLrcAsync = async (state: any, playMusicId: number) => {
|
||||
const loadLrcAsync = async (state: any, playMusicId: any) => {
|
||||
if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,15 +217,15 @@ const scrollToPlayList = (val: boolean) => {
|
||||
|
||||
// 收藏功能
|
||||
const isFavorite = computed(() => {
|
||||
return playerStore.favoriteList.includes(playMusic.value.id);
|
||||
return playerStore.favoriteList.includes(playMusic.value.id as number);
|
||||
});
|
||||
|
||||
const toggleFavorite = () => {
|
||||
console.log('isFavorite.value', isFavorite.value);
|
||||
if (isFavorite.value) {
|
||||
playerStore.removeFromFavorite(playMusic.value.id);
|
||||
playerStore.removeFromFavorite(playMusic.value.id as number);
|
||||
} else {
|
||||
playerStore.addToFavorite(playMusic.value.id);
|
||||
playerStore.addToFavorite(playMusic.value.id as number);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -204,6 +205,7 @@ import MusicFull from './MusicFull.vue';
|
||||
const playerStore = usePlayerStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
// 是否播放
|
||||
const play = computed(() => playerStore.isPlay);
|
||||
// 播放列表
|
||||
@@ -315,33 +317,43 @@ const MusicFullRef = ref<any>(null);
|
||||
// 播放暂停按钮事件
|
||||
const playMusicEvent = async () => {
|
||||
try {
|
||||
// 检查是否有有效的音乐对象和 URL
|
||||
if (!playMusic.value?.id || !playerStore.playMusicUrl) {
|
||||
console.warn('No valid music or URL available');
|
||||
playerStore.setPlay(playMusic.value);
|
||||
// 检查是否有有效的音乐对象
|
||||
if (!playMusic.value?.id) {
|
||||
console.warn('没有有效的播放对象');
|
||||
return;
|
||||
}
|
||||
|
||||
// 当前处于播放状态 -> 暂停
|
||||
if (play.value) {
|
||||
// 暂停播放
|
||||
if (audioService.getCurrentSound()) {
|
||||
audioService.pause();
|
||||
playerStore.setPlayMusic(false);
|
||||
}
|
||||
} else {
|
||||
// 开始播放
|
||||
if (audioService.getCurrentSound()) {
|
||||
// 如果已经有音频实例,直接播放
|
||||
audioService.play();
|
||||
} else {
|
||||
// 如果没有音频实例,重新创建并播放
|
||||
await audioService.play(playerStore.playMusicUrl, playMusic.value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 当前处于暂停状态 -> 播放
|
||||
// 有音频实例,直接播放
|
||||
if (audioService.getCurrentSound()) {
|
||||
audioService.play();
|
||||
playerStore.setPlayMusic(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有音频实例,重新获取并播放(包括重新获取B站视频URL)
|
||||
try {
|
||||
// 复用当前播放对象,但强制重新获取URL
|
||||
const result = await playerStore.setPlay({ ...playMusic.value, playMusicUrl: undefined });
|
||||
if (result) {
|
||||
playerStore.setPlayMusic(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新获取播放链接失败:', error);
|
||||
message.error(t('player.playFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('播放出错:', error);
|
||||
playerStore.nextPlay();
|
||||
message.error(t('player.playFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -366,15 +378,22 @@ const scrollToPlayList = (val: boolean) => {
|
||||
};
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
return playerStore.favoriteList.includes(playMusic.value.id);
|
||||
// 将id转换为number,兼容B站视频ID
|
||||
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();
|
||||
// 将id转换为number,兼容B站视频ID
|
||||
const numericId =
|
||||
typeof playMusic.value.id === 'string' ? parseInt(playMusic.value.id, 10) : playMusic.value.id;
|
||||
|
||||
if (isFavorite.value) {
|
||||
playerStore.removeFromFavorite(playMusic.value.id);
|
||||
playerStore.removeFromFavorite(numericId);
|
||||
} else {
|
||||
playerStore.addToFavorite(playMusic.value.id);
|
||||
playerStore.addToFavorite(numericId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,17 @@ const otherRouter = [
|
||||
back: true
|
||||
},
|
||||
component: () => import('@/views/artist/detail.vue')
|
||||
},
|
||||
{
|
||||
path: '/bilibili/:bvid',
|
||||
name: 'bilibiliPlayer',
|
||||
meta: {
|
||||
title: 'B站听书',
|
||||
keepAlive: true,
|
||||
showInMenu: false,
|
||||
back: true
|
||||
},
|
||||
component: () => import('@/views/bilibili/BilibiliPlayer.vue')
|
||||
}
|
||||
];
|
||||
export default otherRouter;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { getBilibiliAudioUrl } from '@/api/bilibili';
|
||||
import { getLikedList, getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import type { ILyric, ILyricText, SongResult } from '@/type/music';
|
||||
@@ -24,6 +25,8 @@ function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
// 提取公共函数:获取B站视频URL
|
||||
|
||||
export const getSongUrl = async (
|
||||
id: string | number,
|
||||
songData: SongResult,
|
||||
@@ -35,6 +38,18 @@ export const getSongUrl = async (
|
||||
|
||||
if (songData.source === 'bilibili' && songData.bilibiliData) {
|
||||
console.log('加载B站音频URL');
|
||||
if (!songData.playMusicUrl && songData.bilibiliData.bvid && songData.bilibiliData.cid) {
|
||||
try {
|
||||
songData.playMusicUrl = await getBilibiliAudioUrl(
|
||||
songData.bilibiliData.bvid,
|
||||
songData.bilibiliData.cid
|
||||
);
|
||||
return songData.playMusicUrl;
|
||||
} catch (error) {
|
||||
console.error('重启后获取B站音频URL失败:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return songData.playMusicUrl || '';
|
||||
}
|
||||
|
||||
@@ -249,6 +264,27 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
const currentPlayListIndex = computed(() => playListIndex.value);
|
||||
|
||||
const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {
|
||||
// 处理B站视频,确保URL有效
|
||||
if (music.source === 'bilibili' && music.bilibiliData) {
|
||||
try {
|
||||
console.log('处理B站视频,检查URL有效性');
|
||||
// 清除之前的URL,强制重新获取
|
||||
music.playMusicUrl = undefined;
|
||||
|
||||
// 重新获取B站视频URL
|
||||
if (music.bilibiliData.bvid && music.bilibiliData.cid) {
|
||||
music.playMusicUrl = await getBilibiliAudioUrl(
|
||||
music.bilibiliData.bvid,
|
||||
music.bilibiliData.cid
|
||||
);
|
||||
console.log('获取B站URL成功:', music.playMusicUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取B站音频URL失败:', error);
|
||||
throw error; // 向上抛出错误,让调用者处理
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPlayMusic = await getSongDetail(music);
|
||||
playMusic.value = updatedPlayMusic;
|
||||
playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;
|
||||
@@ -284,9 +320,15 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
};
|
||||
|
||||
const setPlay = async (song: SongResult) => {
|
||||
await handlePlayMusic(song);
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
||||
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
|
||||
try {
|
||||
await handlePlayMusic(song);
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
||||
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('设置播放失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setIsPlay = (value: boolean) => {
|
||||
@@ -349,7 +391,18 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
}
|
||||
|
||||
playListIndex.value = nowPlayListIndex;
|
||||
await handlePlayMusic(playList.value[playListIndex.value]);
|
||||
|
||||
// 获取下一首歌曲
|
||||
const nextSong = playList.value[playListIndex.value];
|
||||
|
||||
// 如果是B站视频,确保重新获取URL
|
||||
if (nextSong.source === 'bilibili' && nextSong.bilibiliData) {
|
||||
// 清除之前的URL,确保重新获取
|
||||
nextSong.playMusicUrl = undefined;
|
||||
console.log('下一首是B站视频,已清除URL强制重新获取');
|
||||
}
|
||||
|
||||
await handlePlayMusic(nextSong);
|
||||
};
|
||||
|
||||
const prevPlay = async () => {
|
||||
@@ -359,7 +412,18 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
}
|
||||
const nowPlayListIndex =
|
||||
(playListIndex.value - 1 + playList.value.length) % playList.value.length;
|
||||
await handlePlayMusic(playList.value[nowPlayListIndex]);
|
||||
|
||||
// 获取上一首歌曲
|
||||
const prevSong = playList.value[nowPlayListIndex];
|
||||
|
||||
// 如果是B站视频,确保重新获取URL
|
||||
if (prevSong.source === 'bilibili' && prevSong.bilibiliData) {
|
||||
// 清除之前的URL,确保重新获取
|
||||
prevSong.playMusicUrl = undefined;
|
||||
console.log('上一首是B站视频,已清除URL强制重新获取');
|
||||
}
|
||||
|
||||
await handlePlayMusic(prevSong);
|
||||
await fetchSongs(playList.value, playListIndex.value - 5, nowPlayListIndex);
|
||||
};
|
||||
|
||||
@@ -392,8 +456,17 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
|
||||
if (savedPlayMusic && Object.keys(savedPlayMusic).length > 0) {
|
||||
try {
|
||||
console.log('恢复上次播放的音乐:', savedPlayMusic.name);
|
||||
console.log('settingStore.setData', settingStore.setData);
|
||||
const isPlaying = settingStore.setData.autoPlay;
|
||||
|
||||
// 如果是B站视频,确保播放URL能够在重启后正确恢复
|
||||
if (savedPlayMusic.source === 'bilibili' && savedPlayMusic.bilibiliData) {
|
||||
console.log('恢复B站视频播放', savedPlayMusic.bilibiliData);
|
||||
// 清除之前可能存在的播放URL,确保重新获取
|
||||
savedPlayMusic.playMusicUrl = undefined;
|
||||
}
|
||||
|
||||
await handlePlayMusic({ ...savedPlayMusic, playMusicUrl: undefined }, isPlaying);
|
||||
|
||||
if (savedProgress) {
|
||||
|
||||
@@ -23,6 +23,9 @@ export interface SongResult {
|
||||
canDislike?: boolean;
|
||||
program?: any;
|
||||
alg?: string;
|
||||
ar: Artist[];
|
||||
al: Album;
|
||||
count: number;
|
||||
playMusicUrl?: string;
|
||||
playLoading?: boolean;
|
||||
lyric?: ILyric;
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface IBilibiliSearchResult {
|
||||
duration: number | string;
|
||||
pubdate: number;
|
||||
ctime: number;
|
||||
author: string;
|
||||
view: number;
|
||||
danmaku: number;
|
||||
owner: {
|
||||
mid: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,150 +1,174 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
v-model:show="showModal"
|
||||
class="bilibili-player-modal"
|
||||
:mask-closable="true"
|
||||
:auto-focus="false"
|
||||
:to="`#layout-main`"
|
||||
preset="card"
|
||||
height="80%"
|
||||
placement="bottom"
|
||||
>
|
||||
<div class="bilibili-player-wrapper">
|
||||
<div class="bilibili-player-header">
|
||||
<div class="title">{{ videoDetail?.title || '加载中...' }}</div>
|
||||
<div class="actions">
|
||||
<n-button quaternary circle @click="closePlayer">
|
||||
<template #icon>
|
||||
<i class="ri-close-line"></i>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="bilibili-player-page">
|
||||
<n-scrollbar class="content-scrollbar">
|
||||
<div class="content-wrapper">
|
||||
<div v-if="isLoading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
<p>听书加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
<p>听书加载中...</p>
|
||||
</div>
|
||||
<div v-else-if="errorMessage" class="error-wrapper">
|
||||
<i class="ri-error-warning-line text-4xl text-red-500"></i>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<n-button type="primary" @click="loadVideoSource">重试</n-button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="error-wrapper">
|
||||
<i class="ri-error-warning-line text-4xl text-red-500"></i>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<n-button type="primary" @click="loadVideoSource">重试</n-button>
|
||||
</div>
|
||||
<div v-else-if="videoDetail" class="bilibili-info-wrapper" :class="mainContentAnimation">
|
||||
<div class="bilibili-cover">
|
||||
<n-image
|
||||
:src="getBilibiliProxyUrl(videoDetail.pic)"
|
||||
class="cover-image"
|
||||
preview-disabled
|
||||
/>
|
||||
<!-- 悬浮的播放按钮 -->
|
||||
<div class="play-overlay">
|
||||
<div class="play-icon-bg" @click="playCurrentAudio">
|
||||
<i class="ri-play-fill"></i>
|
||||
</div>
|
||||
<!-- 固定在右下角的大型播放按钮 -->
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="corner-play-button"
|
||||
:loading="partLoading"
|
||||
@click="playCurrentAudio"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="ri-play-fill"></i>
|
||||
</template>
|
||||
立即播放
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="videoDetail" class="bilibili-info-wrapper">
|
||||
<div class="bilibili-cover">
|
||||
<n-image
|
||||
:src="getBilibiliProxyUrl(videoDetail.pic)"
|
||||
class="cover-image"
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="play-button" @click="playCurrentAudio">
|
||||
<i class="ri-play-fill text-4xl"></i>
|
||||
<div class="video-info">
|
||||
<div class="title">{{ videoDetail?.title || '加载中...' }}</div>
|
||||
|
||||
<div class="author">
|
||||
<i class="ri-user-line mr-1"></i>
|
||||
<span>{{ videoDetail.owner?.name }}</span>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span
|
||||
><i class="ri-play-line mr-1"></i>{{ formatNumber(videoDetail.stat?.view) }}</span
|
||||
>
|
||||
<span
|
||||
><i class="ri-chat-1-line mr-1"></i
|
||||
>{{ formatNumber(videoDetail.stat?.danmaku) }}</span
|
||||
>
|
||||
<span
|
||||
><i class="ri-thumb-up-line mr-1"></i
|
||||
>{{ formatNumber(videoDetail.stat?.like) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>{{ videoDetail.desc }}</p>
|
||||
</div>
|
||||
<div class="duration">
|
||||
<p>总时长: {{ formatTotalDuration(videoDetail.duration) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-info">
|
||||
<div class="author">
|
||||
<i class="ri-user-line mr-1"></i>
|
||||
<span>{{ videoDetail.owner?.name }}</span>
|
||||
<div
|
||||
v-if="videoDetail?.pages && videoDetail.pages.length > 1"
|
||||
class="video-parts"
|
||||
:class="partsListAnimation"
|
||||
>
|
||||
<div class="parts-title">
|
||||
分P列表 (共{{ videoDetail.pages.length }}集)
|
||||
<n-spin v-if="partLoading" size="small" class="ml-2" />
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span><i class="ri-play-line mr-1"></i>{{ formatNumber(videoDetail.stat?.view) }}</span>
|
||||
<span
|
||||
><i class="ri-chat-1-line mr-1"></i
|
||||
>{{ formatNumber(videoDetail.stat?.danmaku) }}</span
|
||||
<div class="parts-list">
|
||||
<n-button
|
||||
v-for="page in videoDetail.pages"
|
||||
:key="page.cid"
|
||||
:type="isCurrentPlayingPage(page) ? 'primary' : 'default'"
|
||||
:disabled="partLoading"
|
||||
size="small"
|
||||
class="part-item"
|
||||
@click="switchPage(page)"
|
||||
>
|
||||
<span
|
||||
><i class="ri-thumb-up-line mr-1"></i>{{ formatNumber(videoDetail.stat?.like) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>{{ videoDetail.desc }}</p>
|
||||
</div>
|
||||
<div class="duration">
|
||||
<p>总时长: {{ formatTotalDuration(videoDetail.duration) }}</p>
|
||||
{{ page.part }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="videoDetail?.pages && videoDetail.pages.length > 1" class="video-parts">
|
||||
<div class="parts-title">分P列表 (共{{ videoDetail.pages.length }}集)</div>
|
||||
<div class="parts-list">
|
||||
<n-button
|
||||
v-for="page in videoDetail.pages"
|
||||
:key="page.cid"
|
||||
:type="currentPage?.cid === page.cid ? 'primary' : 'default'"
|
||||
size="small"
|
||||
class="part-item"
|
||||
@click="switchPage(page)"
|
||||
>
|
||||
{{ page.part }}
|
||||
</n-button>
|
||||
</div>
|
||||
<!-- 底部留白 -->
|
||||
<div class="pb-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</n-drawer>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { getBilibiliPlayUrl, getBilibiliProxyUrl, getBilibiliVideoDetail } from '@/api/bilibili';
|
||||
import { usePlayerStore } from '@/store/modules/player';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import type { IBilibiliPage, IBilibiliVideoDetail } from '@/types/bilibili';
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
bvid?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', value: boolean): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
defineOptions({
|
||||
name: 'BilibiliPlayer'
|
||||
});
|
||||
|
||||
// 使用路由获取参数
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const message = useMessage();
|
||||
const playerStore = usePlayerStore();
|
||||
const isLoading = ref(true);
|
||||
|
||||
// 从路由参数获取bvid
|
||||
const bvid = computed(() => route.params.bvid as string);
|
||||
|
||||
const isLoading = ref(true); // 初始加载状态
|
||||
const partLoading = ref(false); // 分P加载状态,仅影响分P选择
|
||||
const errorMessage = ref('');
|
||||
const videoDetail = ref<IBilibiliVideoDetail | null>(null);
|
||||
const currentPage = ref<IBilibiliPage | null>(null);
|
||||
const audioList = ref<SongResult[]>([]);
|
||||
|
||||
const showModal = computed({
|
||||
get: () => props.show,
|
||||
set: (value) => {
|
||||
emit('update:show', value);
|
||||
if (!value) {
|
||||
emit('close');
|
||||
}
|
||||
// 只在初始加载时应用动画
|
||||
const initialLoadDone = ref(false);
|
||||
const mainContentAnimation = computed(() => {
|
||||
if (!initialLoadDone.value) {
|
||||
return setAnimationClass('animate__fadeInDown');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const partsListAnimation = computed(() => {
|
||||
if (!initialLoadDone.value) {
|
||||
return setAnimationClass('animate__fadeInUp');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
// 监听bvid变化
|
||||
watch(
|
||||
() => props.bvid,
|
||||
() => bvid.value,
|
||||
async (newBvid) => {
|
||||
if (newBvid) {
|
||||
// 新的视频ID,重置初始加载状态
|
||||
initialLoadDone.value = false;
|
||||
await loadVideoDetail(newBvid);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newValue) => {
|
||||
console.log('Modal show changed:', newValue);
|
||||
if (newValue && props.bvid && !videoDetail.value) {
|
||||
loadVideoDetail(props.bvid);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const closePlayer = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
// 组件挂载时加载数据
|
||||
onMounted(async () => {
|
||||
if (bvid.value) {
|
||||
await loadVideoDetail(bvid.value);
|
||||
} else {
|
||||
message.error('视频ID无效');
|
||||
router.back();
|
||||
}
|
||||
});
|
||||
|
||||
const loadVideoDetail = async (bvid: string) => {
|
||||
if (!bvid) return;
|
||||
@@ -176,12 +200,14 @@ const loadVideoDetail = async (bvid: string) => {
|
||||
errorMessage.value = '获取视频详情失败';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
// 标记初始加载完成
|
||||
initialLoadDone.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const loadVideoSource = async () => {
|
||||
if (!props.bvid || !currentPage.value?.cid) {
|
||||
console.error('缺少必要参数:', { bvid: props.bvid, cid: currentPage.value?.cid });
|
||||
if (!bvid.value || !currentPage.value?.cid) {
|
||||
console.error('缺少必要参数:', { bvid: bvid.value, cid: currentPage.value?.cid });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,7 +215,7 @@ const loadVideoSource = async () => {
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
console.log('加载音频源:', props.bvid, currentPage.value.cid);
|
||||
console.log('加载音频源:', bvid.value, currentPage.value.cid);
|
||||
|
||||
// 将当前视频转换为音频格式加入播放列表
|
||||
const tempAudio = createSongFromBilibiliVideo(); // 创建一个临时对象,还没有URL
|
||||
@@ -228,7 +254,7 @@ const loadVideoSource = async () => {
|
||||
}
|
||||
} as any,
|
||||
bilibiliData: {
|
||||
bvid: props.bvid,
|
||||
bvid: bvid.value,
|
||||
cid: page.cid
|
||||
}
|
||||
} as SongResult;
|
||||
@@ -283,18 +309,22 @@ const createSongFromBilibiliVideo = (): SongResult => {
|
||||
}
|
||||
} as any,
|
||||
bilibiliData: {
|
||||
bvid: props.bvid,
|
||||
bvid: bvid.value,
|
||||
cid: currentPage.value.cid
|
||||
}
|
||||
} as SongResult;
|
||||
};
|
||||
|
||||
const loadSongUrl = async (page: IBilibiliPage, songItem: SongResult) => {
|
||||
if (songItem.playMusicUrl) return songItem; // 如果已有URL则直接返回
|
||||
const loadSongUrl = async (
|
||||
page: IBilibiliPage,
|
||||
songItem: SongResult,
|
||||
forceRefresh: boolean = false
|
||||
) => {
|
||||
if (songItem.playMusicUrl && !forceRefresh) return songItem; // 如果已有URL且不强制刷新则直接返回
|
||||
|
||||
try {
|
||||
console.log(`加载分P音频URL: ${page.part}, cid: ${page.cid}`);
|
||||
const res = await getBilibiliPlayUrl(props.bvid!, page.cid);
|
||||
const res = await getBilibiliPlayUrl(bvid.value, page.cid);
|
||||
const playUrlData = res.data;
|
||||
let url = '';
|
||||
|
||||
@@ -319,23 +349,32 @@ const loadSongUrl = async (page: IBilibiliPage, songItem: SongResult) => {
|
||||
};
|
||||
|
||||
const switchPage = async (page: IBilibiliPage) => {
|
||||
if (partLoading.value || currentPage.value?.cid === page.cid) return;
|
||||
|
||||
console.log('切换到分P:', page.part);
|
||||
// 立即更新UI选中状态
|
||||
currentPage.value = page;
|
||||
|
||||
// 查找对应的音频项
|
||||
const audioItem = audioList.value.find((item) => item.bilibiliData?.cid === page.cid);
|
||||
|
||||
if (audioItem && !audioItem.playMusicUrl) {
|
||||
// 如果该分P没有音频URL,则先加载
|
||||
if (audioItem) {
|
||||
// 设置局部加载状态
|
||||
try {
|
||||
isLoading.value = true;
|
||||
await loadSongUrl(page, audioItem);
|
||||
partLoading.value = true;
|
||||
// 每次切换分P都强制重新加载音频URL,以解决之前的URL可能失效的问题
|
||||
await loadSongUrl(page, audioItem, true);
|
||||
// 切换后自动播放
|
||||
playCurrentAudio();
|
||||
} catch (error) {
|
||||
console.error('切换分P时加载音频URL失败:', error);
|
||||
errorMessage.value = '获取音频地址失败,请重试';
|
||||
message.error('获取音频地址失败,请重试');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
partLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
console.error('未找到对应的音频项');
|
||||
message.error('未找到对应的音频,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -361,14 +400,12 @@ const playCurrentAudio = async () => {
|
||||
console.log('准备播放当前选中的分P:', currentAudio.name);
|
||||
|
||||
try {
|
||||
// 加载当前分P的音频URL(如果需要)
|
||||
if (!currentAudio.playMusicUrl) {
|
||||
isLoading.value = true;
|
||||
await loadSongUrl(currentPage.value!, currentAudio);
|
||||
// 每次播放前都强制重新加载当前分P的音频URL(解决可能的URL失效问题)
|
||||
partLoading.value = true;
|
||||
await loadSongUrl(currentPage.value!, currentAudio, true);
|
||||
|
||||
if (!currentAudio.playMusicUrl) {
|
||||
throw new Error('获取音频URL失败');
|
||||
}
|
||||
if (!currentAudio.playMusicUrl) {
|
||||
throw new Error('获取音频URL失败');
|
||||
}
|
||||
|
||||
// 预加载下一个分P的音频URL(如果有)
|
||||
@@ -377,7 +414,7 @@ const playCurrentAudio = async () => {
|
||||
const nextAudio = audioList.value[nextIndex];
|
||||
const nextPage = videoDetail.value!.pages.find((p) => p.cid === nextAudio.bilibiliData?.cid);
|
||||
|
||||
if (nextPage && !nextAudio.playMusicUrl) {
|
||||
if (nextPage) {
|
||||
console.log('预加载下一个分P:', nextPage.part);
|
||||
loadSongUrl(nextPage, nextAudio).catch((e) => console.warn('预加载下一个分P失败:', e));
|
||||
}
|
||||
@@ -390,13 +427,13 @@ const playCurrentAudio = async () => {
|
||||
console.log('播放当前选中的分P:', currentAudio.name, '音频URL:', currentAudio.playMusicUrl);
|
||||
playerStore.setPlayMusic(currentAudio);
|
||||
|
||||
// 关闭模态框
|
||||
closePlayer();
|
||||
// 播放后通知用户已开始播放
|
||||
message.success('已开始播放');
|
||||
} catch (error) {
|
||||
console.error('播放音频失败:', error);
|
||||
errorMessage.value = error instanceof Error ? error.message : '播放失败,请重试';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
partLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -423,27 +460,61 @@ const formatNumber = (num?: number) => {
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
// 判断是否是当前正在播放的分P
|
||||
const isCurrentPlayingPage = (page: IBilibiliPage) => {
|
||||
// 只根据播放器状态判断,不再使用UI选中状态
|
||||
const currentPlayingMusic = playerStore.playMusic as any;
|
||||
if (
|
||||
currentPlayingMusic &&
|
||||
typeof currentPlayingMusic === 'object' &&
|
||||
currentPlayingMusic.bilibiliData
|
||||
) {
|
||||
// 比较当前播放的音频的cid与此分P的cid
|
||||
return (
|
||||
currentPlayingMusic.bilibiliData.cid === page.cid &&
|
||||
currentPlayingMusic.bilibiliData.bvid === bvid.value
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有正在播放的音乐,则使用UI选择状态
|
||||
return currentPage.value?.cid === page.cid;
|
||||
};
|
||||
|
||||
// 监听播放器状态变化,保持分P列表选中状态同步
|
||||
watch(
|
||||
() => playerStore.playMusic,
|
||||
(newMusic: any) => {
|
||||
if (
|
||||
newMusic &&
|
||||
typeof newMusic === 'object' &&
|
||||
newMusic.bilibiliData &&
|
||||
newMusic.bilibiliData.bvid === bvid.value
|
||||
) {
|
||||
// 查找对应的分P
|
||||
const playingPage = videoDetail.value?.pages?.find(
|
||||
(p) => p.cid === newMusic.bilibiliData.cid
|
||||
);
|
||||
|
||||
// 无条件更新UI状态以确保UI状态与播放状态一致
|
||||
if (playingPage) {
|
||||
currentPage.value = playingPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bilibili-player-modal {
|
||||
width: 90vw;
|
||||
max-width: 1000px;
|
||||
}
|
||||
.bilibili-player-page {
|
||||
@apply h-full flex flex-col;
|
||||
|
||||
.bilibili-player-wrapper {
|
||||
@apply flex flex-col p-8 pt-4;
|
||||
}
|
||||
|
||||
.bilibili-player-header {
|
||||
@apply flex justify-between items-center mb-4;
|
||||
|
||||
.title {
|
||||
@apply text-lg font-medium truncate;
|
||||
.content-scrollbar {
|
||||
@apply flex-1 overflow-hidden;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply flex items-center;
|
||||
.content-wrapper {
|
||||
@apply flex flex-col p-4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,8 +528,28 @@ const formatNumber = (num?: number) => {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
@apply absolute inset-0 flex items-center justify-center bg-black/40 text-white opacity-0 hover:opacity-100 transition-opacity cursor-pointer;
|
||||
.play-overlay {
|
||||
@apply absolute inset-0;
|
||||
|
||||
.play-icon-bg {
|
||||
@apply absolute inset-0 flex items-center justify-center bg-black/40 text-white opacity-0 hover:opacity-100 transition-opacity cursor-pointer;
|
||||
|
||||
i {
|
||||
@apply text-4xl;
|
||||
}
|
||||
}
|
||||
|
||||
.corner-play-button {
|
||||
@apply absolute right-3 bottom-3 shadow-lg flex items-center gap-1 px-4 py-1 text-sm transition-all duration-200;
|
||||
|
||||
&:hover {
|
||||
@apply transform scale-110;
|
||||
}
|
||||
|
||||
i {
|
||||
@apply text-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +573,10 @@ const formatNumber = (num?: number) => {
|
||||
.video-info {
|
||||
@apply flex-1 p-4 rounded-lg bg-gray-100 dark:bg-gray-800;
|
||||
|
||||
.title {
|
||||
@apply text-lg font-medium mb-4 text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.author {
|
||||
@apply flex items-center text-sm mb-2;
|
||||
}
|
||||
@@ -505,11 +600,11 @@ const formatNumber = (num?: number) => {
|
||||
@apply mt-4;
|
||||
|
||||
.parts-title {
|
||||
@apply text-sm font-medium mb-2;
|
||||
@apply text-sm font-medium mb-2 flex items-center;
|
||||
}
|
||||
|
||||
.parts-list {
|
||||
@apply flex flex-wrap gap-2 max-h-60 overflow-y-auto pb-20;
|
||||
@apply flex flex-wrap gap-2 max-h-60 overflow-y-auto pb-4;
|
||||
|
||||
.part-item {
|
||||
@apply text-xs mb-2;
|
||||
@@ -63,7 +63,7 @@
|
||||
:class="setAnimationClass('animate__bounceInLeft')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
:selectable="isSelecting"
|
||||
:selected="selectedSongs.includes(song.id)"
|
||||
:selected="selectedSongs.includes(song.id as number)"
|
||||
@play="handlePlay"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
@@ -319,7 +319,7 @@ const isIndeterminate = computed(() => {
|
||||
// 处理全选/取消全选
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
selectedSongs.value = favoriteSongs.value.map((song) => song.id);
|
||||
selectedSongs.value = favoriteSongs.value.map((song) => song.id as number);
|
||||
} else {
|
||||
selectedSongs.value = [];
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const getHistorySongs = async () => {
|
||||
const endIndex = startIndex + pageSize;
|
||||
const currentPageItems = musicList.value.slice(startIndex, endIndex);
|
||||
|
||||
const currentIds = currentPageItems.map((item) => item.id);
|
||||
const currentIds = currentPageItems.map((item) => item.id as number);
|
||||
const res = await getMusicDetail(currentIds);
|
||||
|
||||
if (res.data.songs) {
|
||||
|
||||
@@ -118,8 +118,6 @@
|
||||
</template>
|
||||
</div>
|
||||
</n-layout>
|
||||
<!-- 添加B站视频播放器组件 -->
|
||||
<bilibili-player v-model:show="showBilibiliPlayer" :bvid="currentBvid" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -127,12 +125,11 @@
|
||||
import { useDateFormat } from '@vueuse/core';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { getBilibiliProxyUrl, searchBilibili } from '@/api/bilibili';
|
||||
import { getHotSearch } from '@/api/home';
|
||||
import { getSearch } from '@/api/search';
|
||||
import BilibiliPlayer from '@/components/BilibiliPlayer.vue';
|
||||
import BilibiliItem from '@/components/common/BilibiliItem.vue';
|
||||
import SearchItem from '@/components/common/SearchItem.vue';
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
@@ -149,6 +146,7 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const playerStore = usePlayerStore();
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
@@ -216,10 +214,6 @@ onMounted(() => {
|
||||
|
||||
const hotKeyword = ref(route.query.keyword || t('search.title.searchList'));
|
||||
|
||||
// 显示B站视频播放器
|
||||
const showBilibiliPlayer = ref(false);
|
||||
const currentBvid = ref('');
|
||||
|
||||
const loadSearch = async (keywords: any, type: any = null, isLoadMore = false) => {
|
||||
if (!keywords) return;
|
||||
|
||||
@@ -421,8 +415,8 @@ const handleSearchHistory = (item: { keyword: string; type: number }) => {
|
||||
|
||||
// 处理B站视频播放
|
||||
const handlePlayBilibili = (item: IBilibiliSearchResult) => {
|
||||
currentBvid.value = item.bvid;
|
||||
showBilibiliPlayer.value = true;
|
||||
// 使用路由导航到B站播放页面
|
||||
router.push(`/bilibili/${item.bvid}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user