feat: 重构心动模式与私人FM播放逻辑

- 心动模式从播放模式循环中独立,移至 SearchBar 作为独立按钮
- 新增私人FM自动续播:播放结束后自动获取下一首
- 播放列表设置时自动清除FM模式标志
- 顺序播放模式播放到最后一首后正确停止
- 新增获取关注歌手新歌 API
- 补充心动模式相关 i18n 翻译
This commit is contained in:
alger
2026-03-22 16:49:00 +08:00
parent 7f0b3c6469
commit 2b8378bbae
12 changed files with 127 additions and 38 deletions

View File

@@ -194,7 +194,9 @@ export default {
tabPlaylist: 'Playlist',
tabMv: 'MV',
tabCharts: 'Charts',
cancelSearch: 'Cancel'
cancelSearch: 'Cancel',
intelligenceMode: 'Intelligence Mode',
exitIntelligence: 'Exit Intelligence Mode'
},
titleBar: {
closeTitle: 'Choose how to close',

View File

@@ -194,7 +194,9 @@ export default {
tabPlaylist: 'プレイリスト',
tabMv: 'MV',
tabCharts: 'チャート',
cancelSearch: 'キャンセル'
cancelSearch: 'キャンセル',
intelligenceMode: '心動モード',
exitIntelligence: '心動モードを終了'
},
titleBar: {
closeTitle: '閉じる方法を選択してください',

View File

@@ -193,7 +193,9 @@ export default {
tabPlaylist: '플레이리스트',
tabMv: 'MV',
tabCharts: '차트',
cancelSearch: '취소'
cancelSearch: '취소',
intelligenceMode: '심쿵 모드',
exitIntelligence: '심쿵 모드 종료'
},
titleBar: {
closeTitle: '닫기 방법을 선택해주세요',

View File

@@ -187,7 +187,9 @@ export default {
tabPlaylist: '播放列表',
tabMv: 'MV',
tabCharts: '排行榜',
cancelSearch: '取消'
cancelSearch: '取消',
intelligenceMode: '心动模式',
exitIntelligence: '退出心动模式'
},
titleBar: {
closeTitle: '请选择关闭方式',

View File

@@ -187,7 +187,9 @@ export default {
tabPlaylist: '播放清單',
tabMv: 'MV',
tabCharts: '排行榜',
cancelSearch: '取消'
cancelSearch: '取消',
intelligenceMode: '心動模式',
exitIntelligence: '退出心動模式'
},
titleBar: {
closeTitle: '請選擇關閉方式',

View File

@@ -19,3 +19,8 @@ export const getArtistTopSongs = (params) => {
export const getArtistAlbums = (params) => {
return request.get('/artist/album', { params });
};
// 获取关注歌手新歌
export const getArtistNewSongs = (limit: number = 20) => {
return request.get<any>('/artist/new/song', { params: { limit } });
};

View File

@@ -467,13 +467,44 @@ const setupAudioListeners = () => {
};
// 监听结束
audioService.on('end', () => {
audioService.on('end', async () => {
console.log('音频播放结束事件触发');
clearInterval();
if (getPlayerStore().playMode === 1) {
// 单曲循环模式
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 {
// 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法
getPlayerStore().nextPlay();

View File

@@ -14,7 +14,7 @@ export function usePlayMode() {
// 当前播放模式
const playMode = computed(() => playerStore.playMode);
// 播放模式图标
// 播放模式图标(心动模式已移至 SearchBar不参与循环切换
const playModeIcon = computed(() => {
switch (playMode.value) {
case 0:
@@ -23,8 +23,6 @@ export function usePlayMode() {
return 'ri-repeat-one-line';
case 2:
return 'ri-shuffle-line';
case 3:
return 'ri-heart-pulse-line';
default:
return 'ri-repeat-2-line';
}
@@ -39,8 +37,6 @@ export function usePlayMode() {
return t('player.playBar.playMode.loop');
case 2:
return t('player.playBar.playMode.random');
case 3:
return t('player.playBar.intelligenceMode.title');
default:
return t('player.playBar.playMode.sequence');
}

View File

@@ -105,6 +105,24 @@
</n-badge>
</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>
<template #trigger>
@@ -205,6 +223,7 @@ import Coffee from '@/components/Coffee.vue';
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
import { useDownloadStatus } from '@/hooks/useDownloadStatus';
import { useZoom } from '@/hooks/useZoom';
import { useIntelligenceModeStore } from '@/store/modules/intelligenceMode';
import { useNavTitleStore } from '@/store/modules/navTitle';
import { useSearchStore } from '@/store/modules/search';
import { useSettingsStore } from '@/store/modules/settings';
@@ -223,6 +242,7 @@ const userStore = useUserStore();
const userSetOptions = ref(USER_SET_OPTIONS);
const { t, locale } = useI18n();
const intelligenceModeStore = useIntelligenceModeStore();
const { downloadingCount, navigateToDownloads } = useDownloadStatus();
const showDownloadButton = computed(
() =>
@@ -230,6 +250,17 @@ const showDownloadButton = computed(
);
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 ───────────────────────────────────────
const showBackButton = computed(() => {
const meta = router.currentRoute.value.meta;
@@ -681,6 +712,16 @@ onMounted(() => {
background: rgba(34, 197, 94, 0.08);
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-btn {

View File

@@ -33,8 +33,17 @@ export const usePlayerStore = defineStore('player', () => {
const intelligenceMode = useIntelligenceModeStore();
// 使用 storeToRefs 获取响应式引用
const { play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent } =
storeToRefs(playerCore);
const {
play,
isPlay,
playMusic,
playMusicUrl,
musicFull,
playbackRate,
volume,
userPlayIntent,
isFmPlaying
} = storeToRefs(playerCore);
const { playList, playListIndex, playMode, originalPlayList, playListDrawerVisible } =
storeToRefs(playlist);
@@ -88,6 +97,7 @@ export const usePlayerStore = defineStore('player', () => {
playbackRate,
volume,
userPlayIntent,
isFmPlaying,
// PlayerCore - Computed
currentSong,

View File

@@ -35,6 +35,7 @@ export const usePlayerCoreStore = defineStore(
const playbackRate = ref(1.0);
const volume = ref(1);
const userPlayIntent = ref(false); // 用户是否想要播放
const isFmPlaying = ref(false); // 是否正在播放私人FM
// 音频输出设备
const audioOutputDeviceId = ref<string>(
@@ -689,6 +690,7 @@ export const usePlayerCoreStore = defineStore(
playbackRate,
volume,
userPlayIntent,
isFmPlaying,
audioOutputDeviceId,
availableAudioDevices,

View File

@@ -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) {
playList.value = [];
playListIndex.value = 0;
@@ -373,21 +379,14 @@ export const usePlaylistStore = defineStore(
* 切换播放模式
*/
const togglePlayMode = async () => {
const { useUserStore } = await import('./user');
const userStore = useUserStore();
const wasRandom = playMode.value === 2;
const wasIntelligence = playMode.value === 3;
let newMode = (playMode.value + 1) % 4;
// 如果要切换到心动模式但用户未使用cookie登录则跳过
if (newMode === 3 && (!userStore.user || userStore.loginType !== 'cookie')) {
console.log('跳过心动模式需要cookie登录');
newMode = 0;
}
// 心动模式(3)不参与循环切换,仅通过 SearchBar 入口进入
// 如果当前是心动模式,切换回顺序播放
const newMode = wasIntelligence ? 0 : (playMode.value + 1) % 3;
const isRandom = newMode === 2;
const isIntelligence = newMode === 3;
console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`);
playMode.value = newMode;
@@ -404,15 +403,8 @@ export const usePlaylistStore = defineStore(
console.log('切换出随机模式,恢复原始顺序');
}
// 切换到心动模式
if (isIntelligence && !wasIntelligence) {
console.log('切换到心动模式');
const intelligenceStore = useIntelligenceModeStore();
await intelligenceStore.playIntelligenceMode();
}
// 从心动模式切换出去
if (!isIntelligence && wasIntelligence) {
if (wasIntelligence) {
console.log('退出心动模式');
const intelligenceStore = useIntelligenceModeStore();
intelligenceStore.clearIntelligenceMode(true);
@@ -441,13 +433,15 @@ export const usePlaylistStore = defineStore(
return;
}
// 检查是否是播放列表的最后一首且设置了播放列表结束定时
if (
playMode.value === 0 &&
playListIndex.value === playList.value.length - 1 &&
sleepTimerStore.sleepTimer.type === 'end'
) {
sleepTimerStore.stopPlayback();
// 顺序播放模式:播放到最后一首后停止
if (playMode.value === 0 && playListIndex.value >= playList.value.length - 1) {
if (sleepTimerStore.sleepTimer.type === 'end') {
sleepTimerStore.stopPlayback();
}
console.log('[nextPlay] 顺序播放模式:已播放到最后一首,停止播放');
playerCore.setIsPlay(false);
const { audioService } = await import('@/services/audioService');
audioService.pause();
return;
}