feat: 添加心动模式播放

This commit is contained in:
alger
2025-10-22 22:48:52 +08:00
parent 9bf513d35d
commit 3527da17da
13 changed files with 304 additions and 89 deletions

View File

@@ -66,7 +66,17 @@ export default {
favorite: 'Favorite {name}',
unFavorite: 'Unfavorite {name}',
playbackSpeed: 'Playback Speed',
advancedControls: 'Advanced Controls'
advancedControls: 'Advanced Controls',
intelligenceMode: {
title: 'Intelligence Mode',
needCookieLogin: 'Please login with Cookie method to use Intelligence Mode',
noFavoritePlaylist: 'Favorite playlist not found',
noLikedSongs: 'You have no liked songs yet',
loading: 'Loading Intelligence Mode',
success: 'Loaded {count} songs',
failed: 'Failed to get Intelligence Mode list',
error: 'Intelligence Mode error'
}
},
eq: {
title: 'Equalizer',

View File

@@ -67,7 +67,17 @@ export default {
unFavorite: '{name}をお気に入りから削除しました',
miniPlayBar: 'ミニ再生バー',
playbackSpeed: '再生速度',
advancedControls: 'その他の設定'
advancedControls: 'その他の設定',
intelligenceMode: {
title: 'インテリジェンスモード',
needCookieLogin: 'Cookie方式でログインしてからインテリジェンスモードを使用してください',
noFavoritePlaylist: '「お気に入りの音楽」プレイリストが見つかりません',
noLikedSongs: 'まだ「いいね」した楽曲がありません',
loading: 'インテリジェンスモードを読み込み中',
success: '{count} 曲を読み込みました',
failed: 'インテリジェンスモードのリスト取得に失敗しました',
error: 'インテリジェンスモードの再生でエラーが発生しました'
}
},
eq: {
title: 'イコライザー',

View File

@@ -67,7 +67,17 @@ export default {
unFavorite: '{name} 즐겨찾기 해제됨',
miniPlayBar: '미니 재생바',
playbackSpeed: '재생 속도',
advancedControls: '고급 설정'
advancedControls: '고급 설정',
intelligenceMode: {
title: '인텔리전스 모드',
needCookieLogin: '쿠키 방식으로 로그인한 후 인텔리전스 모드를 사용할 수 있습니다',
noFavoritePlaylist: '내가 좋아하는 음악 재생목록을 찾을 수 없습니다',
noLikedSongs: '아직 좋아한 노래가 없습니다',
loading: '인텔리전스 모드를 불러오는 중',
success: '총 {count}곡을 불러왔습니다',
failed: '인텔리전스 모드 목록을 가져오는 데 실패했습니다',
error: '인텔리전스 모드 재생 오류'
}
},
eq: {
title: '이퀄라이저',

View File

@@ -67,7 +67,17 @@ export default {
unFavorite: '已取消收藏{name}',
miniPlayBar: '迷你播放栏',
playbackSpeed: '播放速度',
advancedControls: '更多设置s'
advancedControls: '更多设置',
intelligenceMode: {
title: '心动模式',
needCookieLogin: '请使用 Cookie 方式登录后使用心动模式',
noFavoritePlaylist: '未找到我喜欢的音乐歌单',
noLikedSongs: '您还没有喜欢的歌曲',
loading: '正在加载心动模式',
success: '已加载 {count} 首歌曲',
failed: '获取心动模式列表失败',
error: '心动模式播放出错'
}
},
eq: {
title: '均衡器',

View File

@@ -67,7 +67,17 @@ export default {
unFavorite: '已取消收藏{name}',
miniPlayBar: '迷你播放列',
playbackSpeed: '播放速度',
advancedControls: '更多設定s'
advancedControls: '更多設定',
intelligenceMode: {
title: '心動模式',
needCookieLogin: '請使用 Cookie 方式登入後使用心動模式',
noFavoritePlaylist: '未找到我喜歡的音樂歌單',
noLikedSongs: '您還沒有喜歡的歌曲',
loading: '正在載入心動模式',
success: '已載入 {count} 首歌曲',
failed: '取得心動模式清單失敗',
error: '心動模式播放出錯'
}
},
eq: {
title: '等化器',

View File

@@ -210,3 +210,15 @@ export function getHistoryRecommendSongs(date: string) {
params: { date }
});
}
/**
* 心动模式/智能播放
* @param params id: 歌曲id, pid: 歌单id, sid: 要开始播放的歌曲id(可选)
*/
export function getIntelligenceList(params: { id: number; pid: number; sid?: number }) {
return request({
url: '/playmode/intelligence/list',
method: 'get',
params
});
}

View File

@@ -343,7 +343,7 @@
<i class="ri-arrow-down-s-line"></i>
</div>
<div class="side-button" @click="togglePlayMode">
<i :class="playModeIcon"></i>
<i :class="[playModeIcon, { 'intelligence-active': playMode === 3 }]"></i>
</div>
<div class="main-button prev" @click="prevSong">
<i class="ri-skip-back-fill"></i>
@@ -382,6 +382,7 @@ import {
useLyricProgress
} from '@/hooks/MusicHook';
import { useArtist } from '@/hooks/useArtist';
import { usePlayMode } from '@/hooks/usePlayMode';
import { usePlayerStore } from '@/store/modules/player';
import { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';
import { getImgUrl, secondToMinute } from '@/utils';
@@ -394,19 +395,9 @@ const playerStore = usePlayerStore();
// 播放控制相关
const play = computed(() => playerStore.isPlay);
const playIcon = computed(() => (play.value ? 'ri-pause-fill' : 'ri-play-fill'));
const playMode = computed(() => playerStore.playMode);
const playModeIcon = computed(() => {
switch (playMode.value) {
case 0:
return 'ri-repeat-line';
case 1:
return 'ri-repeat-one-line';
case 2:
return 'ri-shuffle-line';
default:
return 'ri-repeat-line';
}
});
// 播放模式
const { playMode, playModeIcon, playModeText, togglePlayMode: togglePlayModeBase } = usePlayMode();
// 打开播放列表
const showPlaylist = () => {
playerStore.setPlayListDrawerVisible(true);
@@ -963,12 +954,8 @@ const prevSong = () => {
};
const togglePlayMode = () => {
playerStore.togglePlayMode();
showBottomToast(
[t('player.playMode.sequence'), t('player.playMode.loop'), t('player.playMode.random')][
playMode.value
]
);
togglePlayModeBase();
showBottomToast(playModeText.value);
};
const closeMusicFull = () => {
@@ -1525,6 +1512,10 @@ const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
i {
@apply text-2xl;
color: var(--text-color-primary);
&.intelligence-active {
@apply text-green-500;
}
}
&:hover {

View File

@@ -105,15 +105,23 @@
</div>
<n-tooltip v-if="!isMobile" trigger="hover" :z-index="9999999">
<template #trigger>
<i class="iconfont" :class="playModeIcon" @click="togglePlayMode"></i>
<i
class="iconfont"
:class="[playModeIcon, { 'intelligence-active': playMode === 3 }]"
@click="togglePlayMode"
></i>
</template>
{{ playModeText }}
</n-tooltip>
<n-tooltip v-if="!isMobile" trigger="hover" :z-index="9999999">
<template #trigger>
<i
class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }"
class="iconfont"
:class="{
'like-active': isFavorite,
'ri-heart-3-fill': isFavorite,
'ri-heart-3-line': !isFavorite
}"
@click="toggleFavorite"
></i>
</template>
@@ -174,6 +182,7 @@ import {
textColors
} from '@/hooks/MusicHook';
import { useArtist } from '@/hooks/useArtist';
import { usePlayMode } from '@/hooks/usePlayMode';
import { audioService } from '@/services/audioService';
import { isBilibiliIdMatch, usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
@@ -282,38 +291,10 @@ const handleVolumeWheel = (e: WheelEvent) => {
};
// 播放模式
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 playModeText = computed(() => {
switch (playMode.value) {
case 0:
return t('player.playBar.playMode.sequence');
case 1:
return t('player.playBar.playMode.loop');
case 2:
return t('player.playBar.playMode.random');
default:
return t('player.playBar.playMode.sequence');
}
});
const { playMode, playModeIcon, playModeText, togglePlayMode } = usePlayMode();
// 播放速度控制
const { playbackRate } = storeToRefs(playerStore);
// 切换播放模式
const togglePlayMode = () => {
playerStore.togglePlayMode();
};
function handleNext() {
playerStore.nextPlay();
@@ -645,6 +626,10 @@ const openPlayListDrawer = () => {
@apply text-red-500 hover:text-red-600 !important;
}
.intelligence-active {
@apply text-green-500 hover:text-green-600 !important;
}
.disabled-icon {
@apply opacity-50 cursor-not-allowed !important;
&:hover {

View File

@@ -20,7 +20,10 @@
<div class="controls-section">
<div class="left-controls">
<button class="control-btn small-btn" @click="togglePlayMode">
<i class="iconfont" :class="playModeIcon"></i>
<i
class="iconfont"
:class="[playModeIcon, { 'intelligence-active': playMode === 3 }]"
></i>
</button>
</div>
@@ -73,6 +76,7 @@
import { computed, onMounted, ref, watch } from 'vue';
import { allTime, nowTime, playMusic } from '@/hooks/MusicHook';
import { usePlayMode } from '@/hooks/usePlayMode';
import { audioService } from '@/services/audioService';
import { usePlayerStore } from '@/store/modules/player';
import { secondToMinute } from '@/utils';
@@ -93,24 +97,7 @@ 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 { playMode, playModeIcon, togglePlayMode } = usePlayMode();
// 音量控制
const audioVolume = ref(
@@ -527,4 +514,8 @@ onMounted(() => {
color: var(--fill-color);
text-shadow: 0 0 8px var(--fill-color-transparent);
}
.intelligence-active {
@apply text-green-500;
}
</style>

View File

@@ -758,7 +758,6 @@ export const getLrcTimeRange = (index: number) => ({
watch(
() => lrcArray.value,
(newLrcArray) => {
console.log('lrcArray.value', lrcArray.value);
if (newLrcArray.length > 0 && isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
}

View File

@@ -0,0 +1,60 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { usePlayerStore } from '@/store/modules/player';
/**
* 播放模式相关的 Hook
* 提供播放模式的图标、文本和切换功能
*/
export function usePlayMode() {
const { t } = useI18n();
const playerStore = usePlayerStore();
// 当前播放模式
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';
case 3:
return 'ri-heart-pulse-line';
default:
return 'ri-repeat-2-line';
}
});
// 播放模式文本
const playModeText = computed(() => {
switch (playMode.value) {
case 0:
return t('player.playBar.playMode.sequence');
case 1:
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');
}
});
// 切换播放模式
const togglePlayMode = () => {
playerStore.togglePlayMode();
};
return {
playMode,
playModeIcon,
playModeText,
togglePlayMode
};
}

View File

@@ -853,10 +853,6 @@ class AudioService {
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) {

View File

@@ -330,8 +330,6 @@ export const loadLrc = async (id: string | number): Promise<ILyric> => {
});
}
console.log('lyrics', lyrics);
return {
lrcTimeArray: times,
lrcArray: lyrics,
@@ -537,6 +535,13 @@ export const usePlayerStore = defineStore('player', () => {
// 原始播放列表 - 保存切换到随机模式前的顺序
const originalPlayList = ref<SongResult[]>(getLocalStorageItem('originalPlayList', []));
// 心动模式状态
const isIntelligenceMode = ref(getLocalStorageItem('isIntelligenceMode', false));
const intelligenceModeInfo = ref<{
playlistId: number;
seedSongId: number;
} | null>(getLocalStorageItem('intelligenceModeInfo', null));
// 通用洗牌函数 - Fisher-Yates 算法
const performShuffle = (list: SongResult[], currentSong?: SongResult): SongResult[] => {
if (list.length <= 1) return [...list];
@@ -960,7 +965,19 @@ export const usePlayerStore = defineStore('player', () => {
musicFull.value = value;
};
const setPlayList = (list: SongResult[], keepIndex: boolean = false) => {
const setPlayList = (
list: SongResult[],
keepIndex: boolean = false,
fromIntelligenceMode: boolean = false
) => {
// 如果不是从心动模式调用,则清除心动模式状态
if (!fromIntelligenceMode && isIntelligenceMode.value) {
isIntelligenceMode.value = false;
intelligenceModeInfo.value = null;
localStorage.removeItem('isIntelligenceMode');
localStorage.removeItem('intelligenceModeInfo');
}
if (list.length === 0) {
playList.value = [];
playListIndex.value = 0;
@@ -1341,10 +1358,21 @@ export const usePlayerStore = defineStore('player', () => {
// 节流
const prevPlay = useThrottleFn(_prevPlay, 500);
const togglePlayMode = () => {
const newMode = (playMode.value + 1) % 3;
const togglePlayMode = async () => {
const userStore = useUserStore();
const wasIntelligence = playMode.value === 3;
const newMode = (playMode.value + 1) % 4; // 扩展到4种模式
const wasRandom = playMode.value === 2;
const isRandom = newMode === 2;
const isIntelligence = newMode === 3;
// 如果要切换到心动模式但用户未使用cookie登录则跳过心动模式
if (isIntelligence && (!userStore.user || userStore.loginType !== 'cookie')) {
console.log('跳过心动模式需要cookie登录');
playMode.value = 0; // 跳到顺序模式
localStorage.setItem('playMode', JSON.stringify(playMode.value));
return;
}
playMode.value = newMode;
localStorage.setItem('playMode', JSON.stringify(playMode.value));
@@ -1356,10 +1384,25 @@ export const usePlayerStore = defineStore('player', () => {
}
// 当从随机模式切换出去时,恢复原始顺序
if (!isRandom && wasRandom) {
if (!isRandom && wasRandom && !isIntelligence) {
restoreOriginalOrder();
console.log('切换出随机模式,恢复原始顺序');
}
// 当切换到心动模式时,触发心动模式播放
if (isIntelligence && !wasIntelligence) {
console.log('切换到心动模式');
await playIntelligenceMode();
}
// 当从心动模式切换出去时,清除心动模式状态
if (!isIntelligence && wasIntelligence) {
console.log('退出心动模式');
isIntelligenceMode.value = false;
intelligenceModeInfo.value = null;
localStorage.setItem('isIntelligenceMode', JSON.stringify(false));
localStorage.removeItem('intelligenceModeInfo');
}
};
const addToFavorite = async (id: number | string) => {
@@ -1444,9 +1487,12 @@ export const usePlayerStore = defineStore('player', () => {
const settingStore = useSettingsStore();
const savedPlayList = getLocalStorageItem('playList', []);
const savedPlayMusic = getLocalStorageItem<SongResult | null>('currentPlayMusic', null);
// 恢复心动模式状态
const savedIntelligenceMode = getLocalStorageItem('isIntelligenceMode', false);
if (savedPlayList.length > 0) {
setPlayList(savedPlayList);
// 如果是心动模式,保持状态
setPlayList(savedPlayList, false, savedIntelligenceMode);
// 重启后恢复随机播放状态
if (playMode.value === 2) {
@@ -1747,6 +1793,86 @@ export const usePlayerStore = defineStore('player', () => {
return newVolume;
};
// 心动模式播放
const playIntelligenceMode = async () => {
const userStore = useUserStore();
const { t } = i18n.global;
// 检查是否使用cookie登录
if (!userStore.user || userStore.loginType !== 'cookie') {
message.warning(t('player.playBar.intelligenceMode.needCookieLogin'));
return;
}
try {
// 获取用户歌单列表
if (userStore.playList.length === 0) {
await userStore.initializePlaylist();
}
// 找到"我喜欢的音乐"歌单(通常是第一个歌单)
const favoritePlaylist = userStore.playList.find(
(pl: any) => pl.userId === userStore.user?.userId && pl.specialType === 5
);
if (!favoritePlaylist) {
message.warning(t('player.playBar.intelligenceMode.noFavoritePlaylist'));
return;
}
// 获取喜欢的歌曲列表
const likedListRes = await getLikedList(userStore.user.userId);
const likedIds = likedListRes.data?.ids || [];
if (likedIds.length === 0) {
message.warning(t('player.playBar.intelligenceMode.noLikedSongs'));
return;
}
// 随机选择一首歌曲
const randomSongId = likedIds[Math.floor(Math.random() * likedIds.length)];
// 调用心动模式API
const { getIntelligenceList } = await import('@/api/music');
const res = await getIntelligenceList({
id: randomSongId,
pid: favoritePlaylist.id
});
if (res.data?.data && res.data.data.length > 0) {
const intelligenceSongs = res.data.data.map((item: any) => ({
id: item.id,
name: item.songInfo.name,
picUrl: item.songInfo.al?.picUrl,
source: 'netease' as Platform,
song: item.songInfo,
...item.songInfo,
playLoading: false
}));
// 设置心动模式状态
isIntelligenceMode.value = true;
intelligenceModeInfo.value = {
playlistId: favoritePlaylist.id,
seedSongId: randomSongId
};
playMode.value = 3; // 设置播放模式为心动模式
localStorage.setItem('isIntelligenceMode', JSON.stringify(true));
localStorage.setItem('intelligenceModeInfo', JSON.stringify(intelligenceModeInfo.value));
localStorage.setItem('playMode', JSON.stringify(playMode.value));
// 替换播放列表并开始播放
await setPlayList(intelligenceSongs, false, true);
await handlePlayMusic(intelligenceSongs[0], true);
} else {
message.error(t('player.playBar.intelligenceMode.failed'));
}
} catch (error) {
console.error('心动模式播放失败:', error);
message.error(t('player.playBar.intelligenceMode.error'));
}
};
return {
play,
isPlay,
@@ -1812,6 +1938,11 @@ export const usePlayerStore = defineStore('player', () => {
originalPlayList,
shufflePlayList,
restoreOriginalOrder,
preloadNextSongs
preloadNextSongs,
// 心动模式
playIntelligenceMode,
isIntelligenceMode,
intelligenceModeInfo
};
});