mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-25 08:47:22 +08:00
✨ feat: 重构心动模式与私人FM播放逻辑
- 心动模式从播放模式循环中独立,移至 SearchBar 作为独立按钮 - 新增私人FM自动续播:播放结束后自动获取下一首 - 播放列表设置时自动清除FM模式标志 - 顺序播放模式播放到最后一首后正确停止 - 新增获取关注歌手新歌 API - 补充心动模式相关 i18n 翻译
This commit is contained in:
@@ -194,7 +194,9 @@ export default {
|
|||||||
tabPlaylist: 'Playlist',
|
tabPlaylist: 'Playlist',
|
||||||
tabMv: 'MV',
|
tabMv: 'MV',
|
||||||
tabCharts: 'Charts',
|
tabCharts: 'Charts',
|
||||||
cancelSearch: 'Cancel'
|
cancelSearch: 'Cancel',
|
||||||
|
intelligenceMode: 'Intelligence Mode',
|
||||||
|
exitIntelligence: 'Exit Intelligence Mode'
|
||||||
},
|
},
|
||||||
titleBar: {
|
titleBar: {
|
||||||
closeTitle: 'Choose how to close',
|
closeTitle: 'Choose how to close',
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ export default {
|
|||||||
tabPlaylist: 'プレイリスト',
|
tabPlaylist: 'プレイリスト',
|
||||||
tabMv: 'MV',
|
tabMv: 'MV',
|
||||||
tabCharts: 'チャート',
|
tabCharts: 'チャート',
|
||||||
cancelSearch: 'キャンセル'
|
cancelSearch: 'キャンセル',
|
||||||
|
intelligenceMode: '心動モード',
|
||||||
|
exitIntelligence: '心動モードを終了'
|
||||||
},
|
},
|
||||||
titleBar: {
|
titleBar: {
|
||||||
closeTitle: '閉じる方法を選択してください',
|
closeTitle: '閉じる方法を選択してください',
|
||||||
|
|||||||
@@ -193,7 +193,9 @@ export default {
|
|||||||
tabPlaylist: '플레이리스트',
|
tabPlaylist: '플레이리스트',
|
||||||
tabMv: 'MV',
|
tabMv: 'MV',
|
||||||
tabCharts: '차트',
|
tabCharts: '차트',
|
||||||
cancelSearch: '취소'
|
cancelSearch: '취소',
|
||||||
|
intelligenceMode: '심쿵 모드',
|
||||||
|
exitIntelligence: '심쿵 모드 종료'
|
||||||
},
|
},
|
||||||
titleBar: {
|
titleBar: {
|
||||||
closeTitle: '닫기 방법을 선택해주세요',
|
closeTitle: '닫기 방법을 선택해주세요',
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ export default {
|
|||||||
tabPlaylist: '播放列表',
|
tabPlaylist: '播放列表',
|
||||||
tabMv: 'MV',
|
tabMv: 'MV',
|
||||||
tabCharts: '排行榜',
|
tabCharts: '排行榜',
|
||||||
cancelSearch: '取消'
|
cancelSearch: '取消',
|
||||||
|
intelligenceMode: '心动模式',
|
||||||
|
exitIntelligence: '退出心动模式'
|
||||||
},
|
},
|
||||||
titleBar: {
|
titleBar: {
|
||||||
closeTitle: '请选择关闭方式',
|
closeTitle: '请选择关闭方式',
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ export default {
|
|||||||
tabPlaylist: '播放清單',
|
tabPlaylist: '播放清單',
|
||||||
tabMv: 'MV',
|
tabMv: 'MV',
|
||||||
tabCharts: '排行榜',
|
tabCharts: '排行榜',
|
||||||
cancelSearch: '取消'
|
cancelSearch: '取消',
|
||||||
|
intelligenceMode: '心動模式',
|
||||||
|
exitIntelligence: '退出心動模式'
|
||||||
},
|
},
|
||||||
titleBar: {
|
titleBar: {
|
||||||
closeTitle: '請選擇關閉方式',
|
closeTitle: '請選擇關閉方式',
|
||||||
|
|||||||
@@ -19,3 +19,8 @@ export const getArtistTopSongs = (params) => {
|
|||||||
export const getArtistAlbums = (params) => {
|
export const getArtistAlbums = (params) => {
|
||||||
return request.get('/artist/album', { params });
|
return request.get('/artist/album', { params });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取关注歌手新歌
|
||||||
|
export const getArtistNewSongs = (limit: number = 20) => {
|
||||||
|
return request.get<any>('/artist/new/song', { params: { limit } });
|
||||||
|
};
|
||||||
|
|||||||
@@ -467,13 +467,44 @@ const setupAudioListeners = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 监听结束
|
// 监听结束
|
||||||
audioService.on('end', () => {
|
audioService.on('end', async () => {
|
||||||
console.log('音频播放结束事件触发');
|
console.log('音频播放结束事件触发');
|
||||||
clearInterval();
|
clearInterval();
|
||||||
|
|
||||||
if (getPlayerStore().playMode === 1) {
|
if (getPlayerStore().playMode === 1) {
|
||||||
// 单曲循环模式
|
// 单曲循环模式
|
||||||
replayMusic();
|
replayMusic();
|
||||||
|
} else if (getPlayerStore().isFmPlaying) {
|
||||||
|
// 私人FM模式:自动获取下一首
|
||||||
|
try {
|
||||||
|
const { getPersonalFM } = await import('@/api/home');
|
||||||
|
const res = await getPersonalFM();
|
||||||
|
const songs = res.data?.data;
|
||||||
|
if (Array.isArray(songs) && songs.length > 0) {
|
||||||
|
const song = songs[0];
|
||||||
|
const fmSong = {
|
||||||
|
id: song.id,
|
||||||
|
name: song.name,
|
||||||
|
picUrl: song.al?.picUrl || song.album?.picUrl,
|
||||||
|
ar: song.artists || song.ar,
|
||||||
|
al: song.al || song.album,
|
||||||
|
source: 'netease' as const,
|
||||||
|
song,
|
||||||
|
...song,
|
||||||
|
playLoading: false
|
||||||
|
} as any;
|
||||||
|
const { usePlaylistStore } = await import('@/store/modules/playlist');
|
||||||
|
const playlistStore = usePlaylistStore();
|
||||||
|
playlistStore.setPlayList([fmSong], false, false);
|
||||||
|
getPlayerStore().isFmPlaying = true; // setPlayList 会清除,需重设
|
||||||
|
await getPlayerStore().handlePlayMusic(fmSong, true);
|
||||||
|
} else {
|
||||||
|
getPlayerStore().setIsPlay(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FM自动播放下一首失败:', error);
|
||||||
|
getPlayerStore().setIsPlay(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
|
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
|
||||||
getPlayerStore().nextPlay();
|
getPlayerStore().nextPlay();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function usePlayMode() {
|
|||||||
// 当前播放模式
|
// 当前播放模式
|
||||||
const playMode = computed(() => playerStore.playMode);
|
const playMode = computed(() => playerStore.playMode);
|
||||||
|
|
||||||
// 播放模式图标
|
// 播放模式图标(心动模式已移至 SearchBar,不参与循环切换)
|
||||||
const playModeIcon = computed(() => {
|
const playModeIcon = computed(() => {
|
||||||
switch (playMode.value) {
|
switch (playMode.value) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -23,8 +23,6 @@ export function usePlayMode() {
|
|||||||
return 'ri-repeat-one-line';
|
return 'ri-repeat-one-line';
|
||||||
case 2:
|
case 2:
|
||||||
return 'ri-shuffle-line';
|
return 'ri-shuffle-line';
|
||||||
case 3:
|
|
||||||
return 'ri-heart-pulse-line';
|
|
||||||
default:
|
default:
|
||||||
return 'ri-repeat-2-line';
|
return 'ri-repeat-2-line';
|
||||||
}
|
}
|
||||||
@@ -39,8 +37,6 @@ export function usePlayMode() {
|
|||||||
return t('player.playBar.playMode.loop');
|
return t('player.playBar.playMode.loop');
|
||||||
case 2:
|
case 2:
|
||||||
return t('player.playBar.playMode.random');
|
return t('player.playBar.playMode.random');
|
||||||
case 3:
|
|
||||||
return t('player.playBar.intelligenceMode.title');
|
|
||||||
default:
|
default:
|
||||||
return t('player.playBar.playMode.sequence');
|
return t('player.playBar.playMode.sequence');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,24 @@
|
|||||||
</n-badge>
|
</n-badge>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 心动模式按钮 -->
|
||||||
|
<n-tooltip v-if="showIntelligenceBtn" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
:class="{ 'intelligence-active': isIntelligenceMode }"
|
||||||
|
@click="toggleIntelligenceMode"
|
||||||
|
>
|
||||||
|
<i class="ri-heart-pulse-line" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
{{
|
||||||
|
isIntelligenceMode
|
||||||
|
? t('comp.searchBar.exitIntelligence')
|
||||||
|
: t('comp.searchBar.intelligenceMode')
|
||||||
|
}}
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
<!-- 用户 -->
|
<!-- 用户 -->
|
||||||
<n-popover trigger="hover" placement="bottom-end" :show-arrow="false" raw>
|
<n-popover trigger="hover" placement="bottom-end" :show-arrow="false" raw>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
@@ -205,6 +223,7 @@ import Coffee from '@/components/Coffee.vue';
|
|||||||
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||||
import { useDownloadStatus } from '@/hooks/useDownloadStatus';
|
import { useDownloadStatus } from '@/hooks/useDownloadStatus';
|
||||||
import { useZoom } from '@/hooks/useZoom';
|
import { useZoom } from '@/hooks/useZoom';
|
||||||
|
import { useIntelligenceModeStore } from '@/store/modules/intelligenceMode';
|
||||||
import { useNavTitleStore } from '@/store/modules/navTitle';
|
import { useNavTitleStore } from '@/store/modules/navTitle';
|
||||||
import { useSearchStore } from '@/store/modules/search';
|
import { useSearchStore } from '@/store/modules/search';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
@@ -223,6 +242,7 @@ const userStore = useUserStore();
|
|||||||
const userSetOptions = ref(USER_SET_OPTIONS);
|
const userSetOptions = ref(USER_SET_OPTIONS);
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
|
const intelligenceModeStore = useIntelligenceModeStore();
|
||||||
const { downloadingCount, navigateToDownloads } = useDownloadStatus();
|
const { downloadingCount, navigateToDownloads } = useDownloadStatus();
|
||||||
const showDownloadButton = computed(
|
const showDownloadButton = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -230,6 +250,17 @@ const showDownloadButton = computed(
|
|||||||
);
|
);
|
||||||
const { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, isZoom100 } = useZoom();
|
const { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, isZoom100 } = useZoom();
|
||||||
|
|
||||||
|
// ── 心动模式 ─────────────────────────────────────────
|
||||||
|
const isIntelligenceMode = computed(() => intelligenceModeStore.isIntelligenceMode);
|
||||||
|
const showIntelligenceBtn = computed(() => userStore.user && userStore.loginType === 'cookie');
|
||||||
|
const toggleIntelligenceMode = async () => {
|
||||||
|
if (isIntelligenceMode.value) {
|
||||||
|
intelligenceModeStore.clearIntelligenceMode();
|
||||||
|
} else {
|
||||||
|
await intelligenceModeStore.playIntelligenceMode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Back button ───────────────────────────────────────
|
// ── Back button ───────────────────────────────────────
|
||||||
const showBackButton = computed(() => {
|
const showBackButton = computed(() => {
|
||||||
const meta = router.currentRoute.value.meta;
|
const meta = router.currentRoute.value.meta;
|
||||||
@@ -681,6 +712,16 @@ onMounted(() => {
|
|||||||
background: rgba(34, 197, 94, 0.08);
|
background: rgba(34, 197, 94, 0.08);
|
||||||
color: #22c55e;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
|
.action-btn.intelligence-active {
|
||||||
|
color: #ec4899;
|
||||||
|
border-color: #fbcfe8;
|
||||||
|
background: #fdf2f8;
|
||||||
|
}
|
||||||
|
.dark .action-btn.intelligence-active {
|
||||||
|
color: #ec4899;
|
||||||
|
border-color: #831843;
|
||||||
|
background: rgba(236, 72, 153, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── User button ─────────────────────────────────────── */
|
/* ── User button ─────────────────────────────────────── */
|
||||||
.user-btn {
|
.user-btn {
|
||||||
|
|||||||
@@ -33,8 +33,17 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
const intelligenceMode = useIntelligenceModeStore();
|
const intelligenceMode = useIntelligenceModeStore();
|
||||||
|
|
||||||
// 使用 storeToRefs 获取响应式引用
|
// 使用 storeToRefs 获取响应式引用
|
||||||
const { play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent } =
|
const {
|
||||||
storeToRefs(playerCore);
|
play,
|
||||||
|
isPlay,
|
||||||
|
playMusic,
|
||||||
|
playMusicUrl,
|
||||||
|
musicFull,
|
||||||
|
playbackRate,
|
||||||
|
volume,
|
||||||
|
userPlayIntent,
|
||||||
|
isFmPlaying
|
||||||
|
} = storeToRefs(playerCore);
|
||||||
|
|
||||||
const { playList, playListIndex, playMode, originalPlayList, playListDrawerVisible } =
|
const { playList, playListIndex, playMode, originalPlayList, playListDrawerVisible } =
|
||||||
storeToRefs(playlist);
|
storeToRefs(playlist);
|
||||||
@@ -88,6 +97,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
playbackRate,
|
playbackRate,
|
||||||
volume,
|
volume,
|
||||||
userPlayIntent,
|
userPlayIntent,
|
||||||
|
isFmPlaying,
|
||||||
|
|
||||||
// PlayerCore - Computed
|
// PlayerCore - Computed
|
||||||
currentSong,
|
currentSong,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
const playbackRate = ref(1.0);
|
const playbackRate = ref(1.0);
|
||||||
const volume = ref(1);
|
const volume = ref(1);
|
||||||
const userPlayIntent = ref(false); // 用户是否想要播放
|
const userPlayIntent = ref(false); // 用户是否想要播放
|
||||||
|
const isFmPlaying = ref(false); // 是否正在播放私人FM
|
||||||
|
|
||||||
// 音频输出设备
|
// 音频输出设备
|
||||||
const audioOutputDeviceId = ref<string>(
|
const audioOutputDeviceId = ref<string>(
|
||||||
@@ -689,6 +690,7 @@ export const usePlayerCoreStore = defineStore(
|
|||||||
playbackRate,
|
playbackRate,
|
||||||
volume,
|
volume,
|
||||||
userPlayIntent,
|
userPlayIntent,
|
||||||
|
isFmPlaying,
|
||||||
audioOutputDeviceId,
|
audioOutputDeviceId,
|
||||||
availableAudioDevices,
|
availableAudioDevices,
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,12 @@ export const usePlaylistStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当新播放列表长度>1时,清除FM模式标志(FM播放列表只有1首)
|
||||||
|
if (list.length > 1) {
|
||||||
|
const playerCore = usePlayerCoreStore();
|
||||||
|
playerCore.isFmPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
playList.value = [];
|
playList.value = [];
|
||||||
playListIndex.value = 0;
|
playListIndex.value = 0;
|
||||||
@@ -373,21 +379,14 @@ export const usePlaylistStore = defineStore(
|
|||||||
* 切换播放模式
|
* 切换播放模式
|
||||||
*/
|
*/
|
||||||
const togglePlayMode = async () => {
|
const togglePlayMode = async () => {
|
||||||
const { useUserStore } = await import('./user');
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const wasRandom = playMode.value === 2;
|
const wasRandom = playMode.value === 2;
|
||||||
const wasIntelligence = playMode.value === 3;
|
const wasIntelligence = playMode.value === 3;
|
||||||
|
|
||||||
let newMode = (playMode.value + 1) % 4;
|
// 心动模式(3)不参与循环切换,仅通过 SearchBar 入口进入
|
||||||
|
// 如果当前是心动模式,切换回顺序播放
|
||||||
// 如果要切换到心动模式,但用户未使用cookie登录,则跳过
|
const newMode = wasIntelligence ? 0 : (playMode.value + 1) % 3;
|
||||||
if (newMode === 3 && (!userStore.user || userStore.loginType !== 'cookie')) {
|
|
||||||
console.log('跳过心动模式:需要cookie登录');
|
|
||||||
newMode = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRandom = newMode === 2;
|
const isRandom = newMode === 2;
|
||||||
const isIntelligence = newMode === 3;
|
|
||||||
|
|
||||||
console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`);
|
console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`);
|
||||||
playMode.value = newMode;
|
playMode.value = newMode;
|
||||||
@@ -404,15 +403,8 @@ export const usePlaylistStore = defineStore(
|
|||||||
console.log('切换出随机模式,恢复原始顺序');
|
console.log('切换出随机模式,恢复原始顺序');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换到心动模式
|
|
||||||
if (isIntelligence && !wasIntelligence) {
|
|
||||||
console.log('切换到心动模式');
|
|
||||||
const intelligenceStore = useIntelligenceModeStore();
|
|
||||||
await intelligenceStore.playIntelligenceMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从心动模式切换出去
|
// 从心动模式切换出去
|
||||||
if (!isIntelligence && wasIntelligence) {
|
if (wasIntelligence) {
|
||||||
console.log('退出心动模式');
|
console.log('退出心动模式');
|
||||||
const intelligenceStore = useIntelligenceModeStore();
|
const intelligenceStore = useIntelligenceModeStore();
|
||||||
intelligenceStore.clearIntelligenceMode(true);
|
intelligenceStore.clearIntelligenceMode(true);
|
||||||
@@ -441,13 +433,15 @@ export const usePlaylistStore = defineStore(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是播放列表的最后一首且设置了播放列表结束定时
|
// 顺序播放模式:播放到最后一首后停止
|
||||||
if (
|
if (playMode.value === 0 && playListIndex.value >= playList.value.length - 1) {
|
||||||
playMode.value === 0 &&
|
if (sleepTimerStore.sleepTimer.type === 'end') {
|
||||||
playListIndex.value === playList.value.length - 1 &&
|
sleepTimerStore.stopPlayback();
|
||||||
sleepTimerStore.sleepTimer.type === 'end'
|
}
|
||||||
) {
|
console.log('[nextPlay] 顺序播放模式:已播放到最后一首,停止播放');
|
||||||
sleepTimerStore.stopPlayback();
|
playerCore.setIsPlay(false);
|
||||||
|
const { audioService } = await import('@/services/audioService');
|
||||||
|
audioService.pause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user