mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:42:19 +08:00
feat: 添加历史日推功能
This commit is contained in:
@@ -43,6 +43,8 @@ export default {
|
||||
collapse: '收起',
|
||||
songCount: '{count}首',
|
||||
language: '语言',
|
||||
today: '今天',
|
||||
yesterday: '昨天',
|
||||
tray: {
|
||||
show: '显示',
|
||||
quit: '退出',
|
||||
|
||||
@@ -117,7 +117,11 @@ export default {
|
||||
cancelCollect: '取消收藏',
|
||||
addToPlaylist: '添加到播放列表',
|
||||
addToPlaylistSuccess: '添加到播放列表成功',
|
||||
songsAlreadyInPlaylist: '歌曲已存在于播放列表中'
|
||||
songsAlreadyInPlaylist: '歌曲已存在于播放列表中',
|
||||
historyRecommend: '历史日推',
|
||||
fetchDatesFailed: '获取日期列表失败',
|
||||
fetchSongsFailed: '获取歌曲列表失败',
|
||||
noSongs: '暂无歌曲'
|
||||
},
|
||||
playlist: {
|
||||
import: {
|
||||
|
||||
@@ -188,3 +188,25 @@ export function subscribeAlbum(params: { t: number; id: number }) {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史日推可用日期列表
|
||||
*/
|
||||
export function getHistoryRecommendDates() {
|
||||
return request({
|
||||
url: '/history/recommend/songs',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史日推详情数据
|
||||
* @param date 日期,格式:YYYY-MM-DD
|
||||
*/
|
||||
export function getHistoryRecommendSongs(date: string) {
|
||||
return request({
|
||||
url: '/history/recommend/songs/detail',
|
||||
method: 'get',
|
||||
params: { date }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<template #content>
|
||||
<div class="song-item-content-compact">
|
||||
<div class="song-item-content-compact-wrapper">
|
||||
<div class="song-item-content-compact-title w-60 flex-shrink-0">
|
||||
<div class="song-item-content-compact-title">
|
||||
<n-ellipsis
|
||||
class="text-ellipsis"
|
||||
line-clamp="1"
|
||||
@@ -197,26 +197,26 @@ const formatDuration = (ms: number): string => {
|
||||
}
|
||||
|
||||
.song-item-content-compact {
|
||||
@apply flex-1 flex items-center gap-4;
|
||||
@apply flex-1 flex items-center gap-2;
|
||||
|
||||
&-wrapper {
|
||||
@apply flex-1 min-w-0 flex items-center;
|
||||
@apply flex-[2] flex items-center gap-2 min-w-0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply text-sm cursor-pointer text-gray-900 dark:text-white flex items-center;
|
||||
@apply flex-[2.5] min-w-0 text-sm cursor-pointer text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
&-artist {
|
||||
@apply w-40 text-sm text-gray-500 dark:text-gray-400 ml-2 flex items-center;
|
||||
@apply flex-[1.5] min-w-0 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-album {
|
||||
@apply w-32 flex items-center text-sm text-gray-500 dark:text-gray-400;
|
||||
@apply flex-[1.5] min-w-0 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-duration {
|
||||
@apply w-16 flex items-center text-sm text-gray-500 dark:text-gray-400 text-right;
|
||||
@apply w-14 flex-shrink-0 text-sm text-gray-500 dark:text-gray-400 justify-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,17 @@ const otherRouter = [
|
||||
back: true
|
||||
},
|
||||
component: () => import('@/views/heatmap/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/history-recommend',
|
||||
name: 'historyRecommend',
|
||||
meta: {
|
||||
title: '历史日推',
|
||||
keepAlive: true,
|
||||
showInMenu: false,
|
||||
back: true
|
||||
},
|
||||
component: () => import('@/views/music/HistoryRecommend.vue')
|
||||
}
|
||||
];
|
||||
export default otherRouter;
|
||||
|
||||
@@ -149,6 +149,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
return collectedAlbumIds.value.has(albumId);
|
||||
};
|
||||
|
||||
// 判断用户是否为VIP
|
||||
const isVip = computed(() => {
|
||||
if (!user.value) return false;
|
||||
// vipType: 0 非VIP, 11 VIP
|
||||
return user.value.vipType && user.value.vipType !== 0;
|
||||
});
|
||||
|
||||
// 初始化
|
||||
const initializeUser = async () => {
|
||||
const savedUser = getLocalStorageItem<UserData | null>('user', null);
|
||||
@@ -184,6 +191,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
collectedAlbumIds,
|
||||
playList,
|
||||
albumList,
|
||||
isVip,
|
||||
|
||||
// 方法
|
||||
setUser,
|
||||
|
||||
379
src/renderer/views/music/HistoryRecommend.vue
Normal file
379
src/renderer/views/music/HistoryRecommend.vue
Normal file
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="history-recommend-page">
|
||||
<!-- 头部标题和操作按钮 -->
|
||||
<div class="music-header h-12 flex items-center justify-between">
|
||||
<n-ellipsis :line-clamp="1" class="flex-shrink-0 mr-3">
|
||||
<div class="music-title">
|
||||
{{ t('comp.musicList.historyRecommend') }}
|
||||
</div>
|
||||
</n-ellipsis>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex-grow flex-1 flex items-center justify-end gap-2">
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="action-button hover-green" @click="handlePlayAll">
|
||||
<i class="icon iconfont ri-play-fill"></i>
|
||||
</div>
|
||||
</template>
|
||||
{{ t('comp.musicList.playAll') }}
|
||||
</n-tooltip>
|
||||
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="action-button hover-green" @click="addToPlaylist">
|
||||
<i class="icon iconfont ri-add-line"></i>
|
||||
</div>
|
||||
</template>
|
||||
{{ t('comp.musicList.addToPlaylist') }}
|
||||
</n-tooltip>
|
||||
|
||||
<!-- 布局切换按钮 -->
|
||||
<div class="layout-toggle" v-if="!isMobile">
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="toggle-button hover-green" @click="toggleLayout">
|
||||
<i
|
||||
class="icon iconfont"
|
||||
:class="isCompactLayout ? 'ri-list-check-2' : 'ri-grid-line'"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
{{
|
||||
isCompactLayout
|
||||
? t('comp.musicList.switchToNormal')
|
||||
: t('comp.musicList.switchToCompact')
|
||||
}}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择标签 -->
|
||||
<div v-if="availableDates.length > 0" class="date-tabs-wrapper">
|
||||
<n-tabs
|
||||
v-model:value="selectedDate"
|
||||
type="segment"
|
||||
animated
|
||||
size="large"
|
||||
@update:value="handleDateChange"
|
||||
>
|
||||
<n-tab
|
||||
v-for="date in displayedDates"
|
||||
:key="date"
|
||||
:name="date"
|
||||
:tab="formatDate(date)"
|
||||
></n-tab>
|
||||
</n-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 歌曲列表内容 -->
|
||||
<div class="music-content">
|
||||
<n-spin :show="loadingDates || loadingSongs">
|
||||
<!-- 歌曲列表 -->
|
||||
<div v-if="songs.length > 0" class="music-list-container">
|
||||
<div class="music-list">
|
||||
<div class="music-list-content">
|
||||
<!-- 使用虚拟列表 -->
|
||||
<n-virtual-list
|
||||
class="song-virtual-list"
|
||||
style="max-height: calc(100vh - 200px)"
|
||||
:items="songs"
|
||||
:item-size="isCompactLayout ? 50 : 70"
|
||||
item-resizable
|
||||
key-field="id"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div>
|
||||
<div class="double-item">
|
||||
<song-item
|
||||
:index="index"
|
||||
:compact="isCompactLayout"
|
||||
:item="formatSong(item)"
|
||||
@play="handlePlay"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="index === songs.length - 1" class="h-36"></div>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="!loadingSongs && selectedDate" class="empty-state">
|
||||
<i class="icon iconfont ri-disc-line"></i>
|
||||
<p>{{ t('comp.musicList.noSongs') }}</p>
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
<play-bottom />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { getHistoryRecommendDates, getHistoryRecommendSongs } from '@/api/music';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { usePlayerStore } from '@/store';
|
||||
import type { SongResult } from '@/types/music';
|
||||
import { isMobile } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const playerStore = usePlayerStore();
|
||||
|
||||
// 状态
|
||||
const availableDates = ref<string[]>([]);
|
||||
const selectedDate = ref<string>('');
|
||||
const songs = ref<SongResult[]>([]);
|
||||
const loadingDates = ref(false);
|
||||
const loadingSongs = ref(false);
|
||||
const isCompactLayout = ref(
|
||||
isMobile.value ? false : localStorage.getItem('musicListLayout') === 'compact'
|
||||
);
|
||||
|
||||
// 只显示最近的10个日期
|
||||
const displayedDates = computed(() => {
|
||||
return availableDates.value.slice(0, 10);
|
||||
});
|
||||
|
||||
// 格式化日期显示
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
// 判断是否是今天或昨天
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return t('common.today');
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return t('common.yesterday');
|
||||
}
|
||||
|
||||
// 格式化为 MM月DD日
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
};
|
||||
|
||||
// 格式化歌曲数据
|
||||
const formatSong = (item: any) => {
|
||||
if (!item) return null;
|
||||
return {
|
||||
...item,
|
||||
picUrl: item.al?.picUrl || item.album?.picUrl || item.picUrl,
|
||||
song: {
|
||||
artists: item.ar || item.artists || [],
|
||||
name: item.al?.name || item.album?.name || item.name,
|
||||
id: item.al?.id || item.album?.id || item.id
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 获取可用日期列表
|
||||
const fetchAvailableDates = async () => {
|
||||
try {
|
||||
loadingDates.value = true;
|
||||
const { data } = await getHistoryRecommendDates();
|
||||
if (data?.data?.dates) {
|
||||
availableDates.value = data.data.dates;
|
||||
// 默认选择第一个日期(最近的日期)
|
||||
if (availableDates.value.length > 0) {
|
||||
selectedDate.value = availableDates.value[0];
|
||||
await fetchSongsByDate(selectedDate.value);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史日推日期列表失败:', error);
|
||||
message.error(t('comp.musicList.fetchDatesFailed'));
|
||||
} finally {
|
||||
loadingDates.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据日期获取歌曲列表
|
||||
const fetchSongsByDate = async (date: string) => {
|
||||
try {
|
||||
loadingSongs.value = true;
|
||||
const { data } = await getHistoryRecommendSongs(date);
|
||||
if (data?.data?.songs) {
|
||||
songs.value = data.data.songs;
|
||||
} else {
|
||||
songs.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史日推歌曲失败:', error);
|
||||
message.error(t('comp.musicList.fetchSongsFailed'));
|
||||
songs.value = [];
|
||||
} finally {
|
||||
loadingSongs.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理日期变化
|
||||
const handleDateChange = async (date: string) => {
|
||||
selectedDate.value = date;
|
||||
await fetchSongsByDate(date);
|
||||
};
|
||||
|
||||
// 切换布局
|
||||
const toggleLayout = () => {
|
||||
isCompactLayout.value = !isCompactLayout.value;
|
||||
localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal');
|
||||
};
|
||||
|
||||
// 添加到播放列表末尾
|
||||
const addToPlaylist = () => {
|
||||
if (songs.value.length === 0) return;
|
||||
|
||||
// 获取当前播放列表
|
||||
const currentList = playerStore.playList;
|
||||
|
||||
// 添加歌曲到播放列表(避免重复添加)
|
||||
const newSongs = songs.value.filter((song) => !currentList.some((item) => item.id === song.id));
|
||||
|
||||
if (newSongs.length === 0) {
|
||||
message.info(t('comp.musicList.songsAlreadyInPlaylist'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 合并到当前播放列表末尾
|
||||
const newList = [...currentList, ...newSongs.map(formatSong)];
|
||||
playerStore.setPlayList(newList);
|
||||
|
||||
message.success(t('comp.musicList.addToPlaylistSuccess', { count: newSongs.length }));
|
||||
};
|
||||
|
||||
// 播放单首歌曲
|
||||
const handlePlay = () => {
|
||||
if (songs.value.length === 0) return;
|
||||
playerStore.setPlayList(songs.value.map(formatSong));
|
||||
};
|
||||
|
||||
// 播放全部
|
||||
const handlePlayAll = () => {
|
||||
if (songs.value.length === 0) return;
|
||||
playerStore.setPlayList(songs.value.map(formatSong));
|
||||
playerStore.setPlay(formatSong(songs.value[0]));
|
||||
};
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchAvailableDates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.history-recommend-page {
|
||||
@apply h-full bg-light-100 dark:bg-dark-100 px-4 mr-2 rounded-2xl;
|
||||
}
|
||||
|
||||
.music {
|
||||
&-header {
|
||||
@apply h-12 flex items-center justify-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply text-xl font-bold text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply h-[calc(100%-60px)];
|
||||
}
|
||||
|
||||
&-list {
|
||||
@apply flex-grow min-h-0;
|
||||
&-container {
|
||||
@apply flex-grow min-h-0 flex flex-col relative w-full;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply min-h-[calc(80vh-60px)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-tabs-wrapper {
|
||||
@apply px-0 mb-4;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
@apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400;
|
||||
|
||||
.icon {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
&.hover-green:hover {
|
||||
.icon {
|
||||
@apply text-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 虚拟列表样式 */
|
||||
.song-virtual-list {
|
||||
@apply w-full;
|
||||
:deep(.n-virtual-list__scroll) {
|
||||
scrollbar-width: thin;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-400 dark:bg-gray-600 rounded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.double-item {
|
||||
@apply w-full mb-2 bg-light-200 bg-opacity-30 dark:bg-dark-200 dark:bg-opacity-20 rounded-3xl;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@apply flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-600 py-20;
|
||||
|
||||
.icon {
|
||||
@apply text-6xl mb-4;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-tabs-rail) {
|
||||
@apply rounded-xl overflow-hidden !important;
|
||||
.n-tabs-capsule {
|
||||
@apply rounded-xl !important;
|
||||
}
|
||||
}
|
||||
|
||||
.date-tabs-wrapper {
|
||||
:deep(.n-tabs-rail) {
|
||||
@apply rounded-xl overflow-hidden bg-white dark:bg-dark-300 !important;
|
||||
.n-tabs-capsule {
|
||||
@apply rounded-xl bg-green-500 dark:bg-green-600 !important;
|
||||
}
|
||||
.n-tabs-tab--active {
|
||||
@apply text-white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-toggle {
|
||||
.toggle-button {
|
||||
@apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors;
|
||||
|
||||
.icon {
|
||||
@apply text-lg text-gray-500 dark:text-gray-400 transition-colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -152,6 +152,15 @@
|
||||
:class="setAnimationClass('animate__fadeIn')"
|
||||
object-fit="cover"
|
||||
/>
|
||||
|
||||
<div v-if="isDailyRecommend && userStore.isVip" class="history-recommend-btn">
|
||||
<n-button tertiary round type="primary" size="small" @click="goToHistoryRecommend">
|
||||
<template #icon>
|
||||
<i class="icon iconfont ri-history-line"></i>
|
||||
</template>
|
||||
{{ t('comp.musicList.historyRecommend') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 歌单显示创建者,专辑显示艺术家 -->
|
||||
<div v-if="isAlbum && listInfo?.artist" class="creator-info">
|
||||
@@ -229,7 +238,7 @@ import { useMessage } from 'naive-ui';
|
||||
import PinyinMatch from 'pinyin-match';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
getMusicDetail,
|
||||
@@ -249,6 +258,7 @@ import { getLoginErrorMessage, hasPermission } from '@/utils/auth';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const playerStore = usePlayerStore();
|
||||
const musicStore = useMusicStore();
|
||||
const recommendStore = useRecommendStore();
|
||||
@@ -1026,6 +1036,11 @@ const handleBatchDownload = async () => {
|
||||
await batchDownloadMusic(selectedSongsList);
|
||||
cancelSelect();
|
||||
};
|
||||
|
||||
// 跳转到历史日推页面
|
||||
const goToHistoryRecommend = () => {
|
||||
router.push({ name: 'historyRecommend' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1057,12 +1072,19 @@ const handleBatchDownload = async () => {
|
||||
@apply w-[25%] flex-shrink-0 pr-8 flex flex-col;
|
||||
|
||||
.music-cover {
|
||||
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px];
|
||||
@apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px] relative;
|
||||
.cover-img {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
}
|
||||
|
||||
.history-recommend-btn {
|
||||
@apply mb-4 absolute bottom-1 right-4 z-10;
|
||||
:deep(.n-button) {
|
||||
@apply w-full bg-black bg-opacity-30 text-green-400 hover:bg-opacity-50 hover:text-green-500 transition-colors;
|
||||
}
|
||||
}
|
||||
|
||||
.creator-info {
|
||||
@apply flex items-center mb-4;
|
||||
.creator-name {
|
||||
|
||||
Reference in New Issue
Block a user