feat: 扩展数据层与播放能力

This commit is contained in:
alger
2026-02-04 20:10:28 +08:00
parent a44addef22
commit 3a3820cf52
29 changed files with 1111 additions and 675 deletions
+25 -2
View File
@@ -98,7 +98,6 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
setLocalStorageItem('isIntelligenceMode', true);
setLocalStorageItem('intelligenceModeInfo', intelligenceModeInfo.value);
setLocalStorageItem('playMode', playlistStore.playMode);
// 替换播放列表并开始播放
playlistStore.setPlayList(intelligenceSongs, false, true);
@@ -114,12 +113,36 @@ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => {
/**
* 清除心动模式状态
* @param skipPlayModeChange 是否跳过播放模式切换
*/
const clearIntelligenceMode = () => {
const clearIntelligenceMode = (skipPlayModeChange: boolean = false) => {
console.log(
'[IntelligenceMode] clearIntelligenceMode 被调用,skipPlayModeChange:',
skipPlayModeChange
);
isIntelligenceMode.value = false;
intelligenceModeInfo.value = null;
setLocalStorageItem('isIntelligenceMode', false);
localStorage.removeItem('intelligenceModeInfo');
console.log(
'[IntelligenceMode] 心动模式状态已清除,isIntelligenceMode:',
isIntelligenceMode.value
);
// 自动切换播放模式为顺序播放 (playMode = 0)
if (!skipPlayModeChange) {
(async () => {
const { usePlaylistStore } = await import('./playlist');
const playlistStore = usePlaylistStore();
if (playlistStore.playMode === 3) {
console.log('[IntelligenceMode] 退出心动模式,自动切换播放模式为顺序播放');
playlistStore.playMode = 0;
}
})();
}
};
return {
+8
View File
@@ -24,6 +24,14 @@ export const useMusicStore = defineStore('music', {
this.canRemoveSong = canRemove;
},
// 仅设置基础信息(用于先导航后获取数据)
setBasicListInfo(name: string, listInfo: any = null, canRemove = false) {
this.currentMusicList = null; // 标识数据未加载
this.currentMusicListName = name;
this.currentListInfo = listInfo;
this.canRemoveSong = canRemove;
},
// 清除当前音乐列表
clearCurrentMusicList() {
this.currentMusicList = null;
+60 -46
View File
@@ -4,19 +4,21 @@ import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import i18n from '@/../i18n/renderer';
import { getBilibiliAudioUrl } from '@/api/bilibili';
import { getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { usePodcastHistory } from '@/hooks/PodcastHistoryHook';
import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';
import { audioService } from '@/services/audioService';
import { playbackRequestManager } from '@/services/playbackRequestManager';
import { preloadService } from '@/services/preloadService';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { AudioOutputDevice } from '@/types/audio';
import type { Platform, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
const musicHistory = useMusicHistory();
const podcastHistory = usePodcastHistory();
const { message } = createDiscreteApi(['message']);
/**
@@ -36,6 +38,12 @@ export const usePlayerCoreStore = defineStore(
const volume = ref(1);
const userPlayIntent = ref(false); // 用户是否想要播放
// 音频输出设备
const audioOutputDeviceId = ref<string>(
localStorage.getItem('audioOutputDeviceId') || 'default'
);
const availableAudioDevices = ref<AudioOutputDevice[]>([]);
let checkPlayTime: NodeJS.Timeout | null = null;
// ==================== Computed ====================
@@ -239,14 +247,18 @@ export const usePlayerCoreStore = defineStore(
(prev: string, curr: any) => `${prev}${curr.name}/`,
''
)}`;
} else if (music.source === 'bilibili' && music?.song?.ar?.[0]) {
title += ` - ${music.song.ar[0].name}`;
}
document.title = 'AlgerMusic - ' + title;
try {
// 添加到历史记录
musicHistory.addMusic(music);
if (music.isPodcast) {
if (music.program) {
podcastHistory.addPodcast(music.program);
}
} else {
musicHistory.addMusic(music);
}
// 获取歌曲详情
const updatedPlayMusic = await getSongDetail(originalMusic, requestId);
@@ -352,36 +364,6 @@ export const usePlayerCoreStore = defineStore(
console.log('[playAudio] 恢复播放进度:', initialPosition);
}
// B站视频URL检查
if (
playMusic.value.source === 'bilibili' &&
(!playMusicUrl.value || playMusicUrl.value === 'undefined')
) {
console.log('B站视频URL无效,尝试重新获取');
if (playMusic.value.bilibiliData) {
try {
const proxyUrl = await getBilibiliAudioUrl(
playMusic.value.bilibiliData.bvid,
playMusic.value.bilibiliData.cid
);
// 再次验证请求
if (requestId && !playbackRequestManager.isRequestValid(requestId)) {
console.log(`[playAudio] 获取B站URL后请求已失效: ${requestId}`);
return null;
}
(playMusic.value as any).playMusicUrl = proxyUrl;
playMusicUrl.value = proxyUrl;
} catch (error) {
console.error('获取B站音频URL失败:', error);
message.error(i18n.global.t('player.playFailed'));
return null;
}
}
}
// 使用 PreloadService 获取音频
// 优先使用已预加载的 sound(通过 consume 获取并从缓存中移除)
// 如果没有预加载,则进行加载
@@ -514,11 +496,6 @@ export const usePlayerCoreStore = defineStore(
return false;
}
if (currentSong.source === 'bilibili') {
console.warn('B站视频不支持重新解析');
return false;
}
// 使用 SongSourceConfigManager 保存配置
SongSourceConfigManager.setConfig(
currentSong.id,
@@ -579,11 +556,6 @@ export const usePlayerCoreStore = defineStore(
console.log('恢复上次播放的音乐:', playMusic.value.name);
const isPlaying = settingStore.setData.autoPlay;
if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData) {
console.log('恢复B站视频播放', playMusic.value.bilibiliData);
playMusic.value.playMusicUrl = undefined;
}
await handlePlayMusic(
{ ...playMusic.value, isFirstPlay: true, playMusicUrl: undefined },
isPlaying
@@ -602,6 +574,43 @@ export const usePlayerCoreStore = defineStore(
}, 2000);
};
// ==================== 音频输出设备管理 ====================
/**
* 刷新可用音频输出设备列表
*/
const refreshAudioDevices = async () => {
availableAudioDevices.value = await audioService.getAudioOutputDevices();
};
/**
* 切换音频输出设备
*/
const setAudioOutputDevice = async (deviceId: string): Promise<boolean> => {
const success = await audioService.setAudioOutputDevice(deviceId);
if (success) {
audioOutputDeviceId.value = deviceId;
}
return success;
};
/**
* 初始化设备变化监听
*/
const initAudioDeviceListener = () => {
if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', async () => {
await refreshAudioDevices();
const exists = availableAudioDevices.value.some(
(d) => d.deviceId === audioOutputDeviceId.value
);
if (!exists && audioOutputDeviceId.value !== 'default') {
await setAudioOutputDevice('default');
}
});
}
};
return {
// 状态
play,
@@ -612,6 +621,8 @@ export const usePlayerCoreStore = defineStore(
playbackRate,
volume,
userPlayIntent,
audioOutputDeviceId,
availableAudioDevices,
// Computed
currentSong,
@@ -631,14 +642,17 @@ export const usePlayerCoreStore = defineStore(
handlePause,
checkPlaybackState,
reparseCurrentSong,
initializePlayState
initializePlayState,
refreshAudioDevices,
setAudioOutputDevice,
initAudioDeviceListener
};
},
{
persist: {
key: 'player-core-store',
storage: localStorage,
pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay']
pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay', 'audioOutputDeviceId']
}
}
);
+13 -3
View File
@@ -192,11 +192,21 @@ export const usePlaylistStore = defineStore(
keepIndex: boolean = false,
fromIntelligenceMode: boolean = false
) => {
// 如果不是从心动模式调用,清除心动模式状态
// 如果不是从心动模式调用,清除心动模式状态并切换播放模式
if (!fromIntelligenceMode) {
const intelligenceStore = useIntelligenceModeStore();
console.log('[PlaylistStore.setPlayList] 检查心动模式状态:', {
isIntelligenceMode: intelligenceStore.isIntelligenceMode,
currentPlayMode: playMode.value,
fromIntelligenceMode
});
if (intelligenceStore.isIntelligenceMode) {
intelligenceStore.clearIntelligenceMode();
console.log('[PlaylistStore] 退出心动模式,切换播放模式为顺序播放');
playMode.value = 0;
// 清除心动模式状态
intelligenceStore.clearIntelligenceMode(true);
console.log('[PlaylistStore] 心动模式已退出,新的播放模式:', playMode.value);
}
}
@@ -355,7 +365,7 @@ export const usePlaylistStore = defineStore(
if (!isIntelligence && wasIntelligence) {
console.log('退出心动模式');
const intelligenceStore = useIntelligenceModeStore();
intelligenceStore.clearIntelligenceMode();
intelligenceStore.clearIntelligenceMode(true);
}
};
+166
View File
@@ -0,0 +1,166 @@
import { createDiscreteApi } from 'naive-ui';
import { defineStore } from 'pinia';
import { computed, ref, shallowRef } from 'vue';
import * as podcastApi from '@/api/podcast';
import type { DjCategory, DjProgram, DjRadio } from '@/types/podcast';
const { message } = createDiscreteApi(['message']);
export const usePodcastStore = defineStore(
'podcast',
() => {
const subscribedRadios = shallowRef<DjRadio[]>([]);
const categories = shallowRef<DjCategory[]>([]);
const currentRadio = shallowRef<DjRadio | null>(null);
const currentPrograms = shallowRef<DjProgram[]>([]);
const recommendRadios = shallowRef<DjRadio[]>([]);
const todayPerfered = shallowRef<DjProgram[]>([]);
const recentPrograms = shallowRef<DjProgram[]>([]);
const isLoading = ref(false);
const subscribedCount = computed(() => subscribedRadios.value.length);
const isRadioSubscribed = computed(() => {
return (rid: number) => subscribedRadios.value.some((r) => r.id === rid);
});
const fetchSubscribedRadios = async () => {
try {
isLoading.value = true;
const res = await podcastApi.getDjSublist();
subscribedRadios.value = res.data?.djRadios || [];
} catch (error) {
console.error('获取订阅列表失败:', error);
message.error('获取订阅列表失败');
} finally {
isLoading.value = false;
}
};
const toggleSubscribe = async (radio: DjRadio) => {
const isSubed = isRadioSubscribed.value(radio.id);
try {
await podcastApi.subscribeDj(radio.id, isSubed ? 0 : 1);
if (isSubed) {
message.success('已取消订阅');
} else {
message.success('订阅成功');
}
await fetchSubscribedRadios();
if (currentRadio.value?.id === radio.id) {
currentRadio.value = { ...currentRadio.value, subed: !isSubed };
}
} catch (error) {
console.error('订阅操作失败:', error);
message.error(isSubed ? '取消订阅失败' : '订阅失败');
}
};
const fetchRadioDetail = async (rid: number) => {
try {
isLoading.value = true;
const res = await podcastApi.getDjDetail(rid);
currentRadio.value = res.data?.data;
if (currentRadio.value) {
currentRadio.value.subed = isRadioSubscribed.value(rid);
}
} catch (error) {
console.error('获取电台详情失败:', error);
message.error('获取电台详情失败');
} finally {
isLoading.value = false;
}
};
const fetchRadioPrograms = async (rid: number, offset = 0) => {
try {
isLoading.value = true;
const res = await podcastApi.getDjProgram(rid, 30, offset);
if (offset === 0) {
currentPrograms.value = res.data?.programs || [];
} else {
currentPrograms.value.push(...(res.data?.programs || []));
}
} catch (error) {
console.error('获取节目列表失败:', error);
message.error('获取节目列表失败');
} finally {
isLoading.value = false;
}
};
const fetchCategories = async () => {
try {
const res = await podcastApi.getDjCategoryList();
categories.value = res.data?.categories || [];
} catch (error) {
console.error('获取分类列表失败:', error);
}
};
const fetchRecommendRadios = async () => {
try {
const res = await podcastApi.getDjRecommend();
recommendRadios.value = res.data?.djRadios || [];
} catch (error) {
console.error('获取推荐电台失败:', error);
}
};
const fetchTodayPerfered = async () => {
try {
const res = await podcastApi.getDjTodayPerfered();
todayPerfered.value = res.data?.data || [];
} catch (error) {
console.error('获取今日优选失败:', error);
}
};
const fetchRecentPrograms = async () => {
try {
const res = await podcastApi.getRecentDj();
recentPrograms.value = res.data?.data?.list || [];
} catch (error) {
console.error('获取最近播放失败:', error);
}
};
const clearCurrentRadio = () => {
currentRadio.value = null;
currentPrograms.value = [];
};
return {
subscribedRadios,
categories,
currentRadio,
currentPrograms,
recommendRadios,
todayPerfered,
recentPrograms,
isLoading,
subscribedCount,
isRadioSubscribed,
fetchSubscribedRadios,
toggleSubscribe,
fetchRadioDetail,
fetchRadioPrograms,
fetchCategories,
fetchRecommendRadios,
fetchTodayPerfered,
fetchRecentPrograms,
clearCurrentRadio
};
},
{
persist: {
key: 'podcast-store',
storage: localStorage,
pick: ['subscribedRadios', 'categories']
}
}
);
+28
View File
@@ -5,8 +5,23 @@ import { getDayRecommend } from '@/api/home';
import type { IDayRecommend } from '@/types/day_recommend';
import type { SongResult } from '@/types/music';
// 获取当前日期字符串 YYYY-MM-DD
const getTodayDateString = (): string => {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
};
export const useRecommendStore = defineStore('recommend', () => {
const dailyRecommendSongs = ref<SongResult[]>([]);
const lastFetchDate = ref<string>('');
// 检查数据是否过期(跨天)
const isDataStale = (): boolean => {
if (!lastFetchDate.value || dailyRecommendSongs.value.length === 0) {
return true;
}
return lastFetchDate.value !== getTodayDateString();
};
const fetchDailyRecommendSongs = async () => {
try {
@@ -15,6 +30,7 @@ export const useRecommendStore = defineStore('recommend', () => {
if (recommendData && Array.isArray(recommendData.dailySongs)) {
dailyRecommendSongs.value = recommendData.dailySongs as any;
lastFetchDate.value = getTodayDateString();
console.log(`[Recommend Store] 已加载 ${recommendData.dailySongs.length} 首每日推荐歌曲。`);
} else {
dailyRecommendSongs.value = [];
@@ -25,6 +41,15 @@ export const useRecommendStore = defineStore('recommend', () => {
}
};
// 如果数据过期则刷新
const refreshIfStale = async (): Promise<boolean> => {
if (isDataStale()) {
await fetchDailyRecommendSongs();
return true;
}
return false;
};
const replaceSongInDailyRecommend = (oldSongId: number | string, newSong: SongResult) => {
const index = dailyRecommendSongs.value.findIndex((song) => song.id === oldSongId);
if (index !== -1) {
@@ -37,7 +62,10 @@ export const useRecommendStore = defineStore('recommend', () => {
return {
dailyRecommendSongs,
lastFetchDate,
isDataStale,
fetchDailyRecommendSongs,
refreshIfStale,
replaceSongInDailyRecommend
};
});
+7 -2
View File
@@ -1,9 +1,10 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { logout } from '@/api/login';
import { getLikedList } from '@/api/music';
import { getUserAlbumSublist, getUserPlaylist } from '@/api/user';
import type { IUserDetail } from '@/types/user';
import { clearLoginStatus } from '@/utils/auth';
interface UserData {
@@ -23,6 +24,8 @@ function getLocalStorageItem<T>(key: string, defaultValue: T): T {
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref<UserData | null>(getLocalStorageItem('user', null));
const userDetail = ref<IUserDetail | null>(null);
const recordList = ref<any[]>([]);
const loginType = ref<'token' | 'cookie' | 'qr' | 'uid' | null>(
getLocalStorageItem('loginType', null)
);
@@ -205,6 +208,8 @@ export const useUserStore = defineStore('user', () => {
initializeCollectedAlbums,
addCollectedAlbum,
removeCollectedAlbum,
isAlbumCollected
isAlbumCollected,
userDetail,
recordList
};
});