mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-15 07:20:49 +08:00
✨ feat: 歌单列表相添加布局切换、播放全部、收藏、添加到播放列表
This commit is contained in:
@@ -104,6 +104,17 @@ export default {
|
||||
},
|
||||
musicList: {
|
||||
searchSongs: 'Search Songs',
|
||||
noSearchResults: 'No search results'
|
||||
noSearchResults: 'No search results',
|
||||
switchToNormal: 'Switch to normal layout',
|
||||
switchToCompact: 'Switch to compact layout',
|
||||
playAll: 'Play All',
|
||||
collect: 'Collect',
|
||||
collectSuccess: 'Collect Success',
|
||||
cancelCollectSuccess: 'Cancel Collect Success',
|
||||
cancelCollect: 'Cancel Collect',
|
||||
addToPlaylist: 'Add to Playlist',
|
||||
addToPlaylistSuccess: 'Add to Playlist Success',
|
||||
operationFailed: 'Operation Failed',
|
||||
songsAlreadyInPlaylist: 'Songs already in playlist'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,6 +102,17 @@ export default {
|
||||
},
|
||||
musicList: {
|
||||
searchSongs: '搜索歌曲',
|
||||
noSearchResults: '没有找到相关歌曲'
|
||||
noSearchResults: '没有找到相关歌曲',
|
||||
switchToNormal: '切换到默认布局',
|
||||
switchToCompact: '切换到紧凑布局',
|
||||
playAll: '播放全部',
|
||||
collect: '收藏',
|
||||
collectSuccess: '收藏成功',
|
||||
cancelCollectSuccess: '取消收藏成功',
|
||||
operationFailed: '操作失败',
|
||||
cancelCollect: '取消收藏',
|
||||
addToPlaylist: '添加到播放列表',
|
||||
addToPlaylistSuccess: '添加到播放列表成功',
|
||||
songsAlreadyInPlaylist: '歌曲已存在于播放列表中'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,3 +201,11 @@ export function getPlaylistDetail(id: string) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function subscribePlaylist(params: { t: number; id: number }) {
|
||||
return request({
|
||||
url: '/playlist/subscribe',
|
||||
method: 'post',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div
|
||||
class="song-item"
|
||||
:class="{ 'song-mini': mini, 'song-list': list }"
|
||||
:class="{ 'song-mini': mini, 'song-list': list, 'song-compact': compact }"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div v-if="compact && index !== undefined" class="song-item-index" :class="{ 'text-green-500': isPlaying }">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div v-if="selectable" class="song-item-select" @click.stop="toggleSelect">
|
||||
<n-checkbox :checked="selected" />
|
||||
</div>
|
||||
<n-image
|
||||
v-if="item.picUrl"
|
||||
v-if="item.picUrl && !compact"
|
||||
ref="songImg"
|
||||
:src="getImgUrl(item.picUrl, '100y100')"
|
||||
class="song-item-img"
|
||||
@@ -18,9 +23,9 @@
|
||||
}"
|
||||
@load="imageLoad"
|
||||
/>
|
||||
<div class="song-item-content">
|
||||
<div class="song-item-content" :class="{ 'song-item-content-compact': compact }">
|
||||
<div v-if="list" class="song-item-content-wrapper">
|
||||
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{
|
||||
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">{{
|
||||
item.name
|
||||
}}</n-ellipsis>
|
||||
<div class="song-item-content-divider">-</div>
|
||||
@@ -35,9 +40,36 @@
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<template v-else-if="compact">
|
||||
<div class="song-item-content-compact-wrapper">
|
||||
<div class="w-60 flex-shrink-0 flex items-center" @dblclick="playMusicEvent(item)">
|
||||
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">
|
||||
{{ item.name }}
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<div class="w-40 flex-shrink-0 song-item-content-compact-artist flex items-center">
|
||||
<n-ellipsis line-clamp="1">
|
||||
<template v-for="(artist, index) in artists" :key="index">
|
||||
<span
|
||||
class="cursor-pointer hover:text-green-500"
|
||||
@click.stop="handleArtistClick(artist.id)"
|
||||
>{{ artist.name }}</span
|
||||
>
|
||||
<span v-if="index < artists.length - 1"> / </span>
|
||||
</template>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-item-content-album flex items-center">
|
||||
<n-ellipsis line-clamp="1">{{ item.al?.name || '-' }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="song-item-content-duration flex items-center">
|
||||
{{ formatDuration(getDuration(item)) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="song-item-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
<div class="song-item-content-title" @dblclick="playMusicEvent(item)">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1" :class="{ 'text-green-500': isPlaying }">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="song-item-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
@@ -53,15 +85,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
||||
<div v-if="favorite" class="song-item-operating-like">
|
||||
<div class="song-item-operating" :class="{
|
||||
'song-item-operating-list': list,
|
||||
'song-item-operating-compact': compact
|
||||
}">
|
||||
<div v-if="favorite" class="song-item-operating-like" :class="{ 'opacity-0': compact && !isHovering && !isFavorite }">
|
||||
<i
|
||||
class="iconfont icon-likefill"
|
||||
:class="{ 'like-active': isFavorite }"
|
||||
@click.stop="toggleFavorite"
|
||||
></i>
|
||||
</div>
|
||||
<n-tooltip v-if="isNext" trigger="hover" :z-index="9999999" :delay="400">
|
||||
<n-tooltip v-if="isNext" trigger="hover" :z-index="9999999" :delay="400">
|
||||
<template #trigger>
|
||||
<div class="song-item-operating-next" @click.stop="handlePlayNext">
|
||||
<i class="iconfont ri-skip-forward-fill"></i>
|
||||
@@ -71,12 +106,15 @@
|
||||
</n-tooltip>
|
||||
<div
|
||||
class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated"
|
||||
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
||||
:class="{ 'bg-green-600': isPlaying, 'animate__flipInY': playLoading, 'opacity-0': compact && !isHovering && !isPlaying }"
|
||||
@click="playMusicEvent(item)"
|
||||
>
|
||||
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
||||
<i v-else class="iconfont icon-playfill"></i>
|
||||
</div>
|
||||
<div v-if="compact" class="song-item-operating-menu" @click.stop="handleMenuClick" :class="{ 'opacity-0': compact && !isHovering && !isPlaying }">
|
||||
<i class="iconfont ri-more-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<n-dropdown
|
||||
v-if="isElectron"
|
||||
@@ -95,7 +133,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { cloneDeep } from 'lodash';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { NImage, NText, useMessage } from 'naive-ui';
|
||||
import { NEllipsis, NImage, useMessage } from 'naive-ui';
|
||||
import { computed, h, inject, ref, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -114,20 +152,24 @@ const props = withDefaults(
|
||||
item: SongResult;
|
||||
mini?: boolean;
|
||||
list?: boolean;
|
||||
compact?: boolean;
|
||||
favorite?: boolean;
|
||||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
canRemove?: boolean;
|
||||
isNext?: boolean;
|
||||
index?: number;
|
||||
}>(),
|
||||
{
|
||||
mini: false,
|
||||
list: false,
|
||||
compact: false,
|
||||
favorite: true,
|
||||
selectable: false,
|
||||
selected: false,
|
||||
canRemove: false,
|
||||
isNext: false
|
||||
isNext: false,
|
||||
index: undefined
|
||||
}
|
||||
);
|
||||
|
||||
@@ -147,6 +189,7 @@ const isPlaying = computed(() => {
|
||||
const showDropdown = ref(false);
|
||||
const dropdownX = ref(0);
|
||||
const dropdownY = ref(0);
|
||||
const isHovering = ref(false);
|
||||
|
||||
const isDownloading = ref(false);
|
||||
|
||||
@@ -172,26 +215,49 @@ const renderSongPreview = () => {
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex-1 min-w-0 py-1'
|
||||
class: 'flex-1 min-w-0 py-1 overflow-hidden'
|
||||
},
|
||||
[
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'mb-1'
|
||||
class: 'mb-1 overflow-hidden'
|
||||
},
|
||||
[
|
||||
h(
|
||||
NText,
|
||||
NEllipsis,
|
||||
{
|
||||
lineClamp: 1,
|
||||
depth: 1,
|
||||
class: 'text-sm font-medium'
|
||||
class: 'text-sm font-medium w-full',
|
||||
style: 'max-width: 150px; min-width: 120px;'
|
||||
},
|
||||
{
|
||||
default: () => props.item.name
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'text-xs text-gray-500 dark:text-gray-400 overflow-hidden'
|
||||
},
|
||||
[
|
||||
h(
|
||||
NEllipsis,
|
||||
{
|
||||
lineClamp: 1,
|
||||
style: 'max-width: 150px;'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(' / ');
|
||||
return artistNames || '未知艺术家';
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -268,6 +334,13 @@ const handleContextMenu = (e: MouseEvent) => {
|
||||
dropdownY.value = e.clientY;
|
||||
};
|
||||
|
||||
const handleMenuClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
showDropdown.value = true;
|
||||
dropdownX.value = e.clientX;
|
||||
dropdownY.value = e.clientY;
|
||||
};
|
||||
|
||||
const handleSelect = (key: string | number) => {
|
||||
showDropdown.value = false;
|
||||
if (key === 'download') {
|
||||
@@ -435,6 +508,33 @@ const handlePlayNext = () => {
|
||||
playerStore.addToNextPlay(props.item);
|
||||
message.success(t('songItem.message.addedToNextPlay'));
|
||||
};
|
||||
|
||||
// 获取歌曲时长
|
||||
const getDuration = (item: SongResult): number => {
|
||||
// 检查各种可能的时长属性路径
|
||||
if (item.duration) return item.duration;
|
||||
if (typeof item.dt === 'number') return item.dt;
|
||||
// 遍历可能存在的其他时长属性路径
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 格式化时长
|
||||
const formatDuration = (ms: number): string => {
|
||||
if (!ms) return '--:--';
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 鼠标悬停事件
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -452,12 +552,23 @@ const handlePlayNext = () => {
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-100 dark:bg-gray-800;
|
||||
|
||||
.song-item-operating-compact {
|
||||
.song-item-operating-like,
|
||||
.song-item-operating-play {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-img {
|
||||
@apply w-12 h-12 rounded-2xl mr-4;
|
||||
}
|
||||
|
||||
&-index {
|
||||
@apply w-8 text-center text-gray-500 dark:text-gray-400 text-sm;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply flex-1;
|
||||
|
||||
@@ -468,6 +579,26 @@ const handlePlayNext = () => {
|
||||
&-name {
|
||||
@apply text-xs text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-compact {
|
||||
@apply flex items-center gap-4;
|
||||
|
||||
&-wrapper {
|
||||
@apply flex-1 min-w-0;
|
||||
}
|
||||
|
||||
&-artist {
|
||||
@apply text-sm text-gray-500 dark:text-gray-400 ml-2;
|
||||
}
|
||||
}
|
||||
|
||||
&-album {
|
||||
@apply w-32 text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-duration {
|
||||
@apply w-16 text-sm text-gray-500 dark:text-gray-400 text-right;
|
||||
}
|
||||
}
|
||||
|
||||
&-operating {
|
||||
@@ -514,6 +645,14 @@ const handlePlayNext = () => {
|
||||
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
@apply cursor-pointer flex items-center justify-center px-2;
|
||||
|
||||
.iconfont {
|
||||
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
@@ -521,6 +660,61 @@ const handlePlayNext = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.song-compact {
|
||||
@apply rounded-lg p-2 h-12 mb-1 border-b dark:border-gray-800 border-gray-100;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-50 dark:bg-gray-700;
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.song-item-content {
|
||||
&-title {
|
||||
@apply text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.song-item-content-compact-wrapper {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.song-item-content-compact-artist {
|
||||
@apply w-40;
|
||||
}
|
||||
|
||||
.song-item-operating-compact {
|
||||
@apply border-none bg-transparent gap-2 flex items-center;
|
||||
|
||||
.song-item-operating-like,
|
||||
.song-item-operating-play {
|
||||
@apply transition-opacity duration-200;
|
||||
}
|
||||
|
||||
.song-item-operating-play {
|
||||
@apply w-7 h-7;
|
||||
|
||||
.iconfont {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
|
||||
.song-item-operating-like {
|
||||
@apply mr-1 ml-0;
|
||||
|
||||
.iconfont {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-mini {
|
||||
@apply p-2 rounded-2xl;
|
||||
|
||||
|
||||
@@ -535,7 +535,9 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
// 记录到本地存储,保持一致性
|
||||
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
||||
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
|
||||
|
||||
if (success) {
|
||||
isPlay.value = true;
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error('设置播放失败:', error);
|
||||
|
||||
@@ -42,6 +42,9 @@ export interface SongResult {
|
||||
expiredAt?: number;
|
||||
// 获取时间
|
||||
createdAt?: number;
|
||||
// 时长
|
||||
duration?: number;
|
||||
dt?: number;
|
||||
}
|
||||
|
||||
export interface Song {
|
||||
|
||||
@@ -7,20 +7,75 @@
|
||||
</div>
|
||||
</n-ellipsis>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="flex-grow flex-1 flex items-center justify-end">
|
||||
<div class="search-container">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
:placeholder="t('comp.musicList.searchSongs')"
|
||||
clearable
|
||||
round
|
||||
size="small"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="icon iconfont ri-search-line text-sm"></i>
|
||||
<!-- 搜索框和布局切换 -->
|
||||
<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 v-if="canCollect" placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="action-button" :class="isCollected ? 'collected' : 'hover-green'" @click="toggleCollect">
|
||||
<i class="icon iconfont" :class="isCollected ? 'ri-heart-fill' : 'ri-heart-line'"></i>
|
||||
</div>
|
||||
</template>
|
||||
{{ isCollected ? t('comp.musicList.cancelCollect') : t('comp.musicList.collect') }}
|
||||
</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">
|
||||
<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>
|
||||
</n-input>
|
||||
{{ isCompactLayout ? t('comp.musicList.switchToNormal') : t('comp.musicList.switchToCompact') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="search-container" :class="{ 'search-expanded': isSearchVisible }">
|
||||
<template v-if="isSearchVisible">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
:placeholder="t('comp.musicList.searchSongs')"
|
||||
clearable
|
||||
round
|
||||
size="small"
|
||||
@blur="handleSearchBlur"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="icon iconfont ri-search-line text-sm"></i>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<i class="icon iconfont ri-close-line text-sm cursor-pointer" @click="closeSearch"></i>
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-tooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="search-button" @click="showSearch">
|
||||
<i class="icon iconfont ri-search-line"></i>
|
||||
</div>
|
||||
</template>
|
||||
{{ t('comp.musicList.searchSongs') }}
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,14 +120,16 @@
|
||||
class="song-virtual-list"
|
||||
style="height: calc(80vh - 60px)"
|
||||
:items="filteredSongs"
|
||||
:item-size="70"
|
||||
:item-size="isCompactLayout ? 50 : 70"
|
||||
item-resizable
|
||||
key-field="id"
|
||||
@scroll="handleVirtualScroll"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<template #default="{ item, index }">
|
||||
<div class="double-item">
|
||||
<song-item
|
||||
<song-item
|
||||
:index="index"
|
||||
:compact="isCompactLayout"
|
||||
:item="formatSong(item)"
|
||||
:can-remove="canRemove"
|
||||
@play="handlePlay"
|
||||
@@ -97,10 +154,10 @@ defineOptions({
|
||||
});
|
||||
|
||||
import PinyinMatch from 'pinyin-match';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { updatePlaylistTracks } from '@/api/music';
|
||||
import { updatePlaylistTracks, subscribePlaylist } from '@/api/music';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
import { getMusicDetail, getMusicListByType } from '@/api/music';
|
||||
@@ -122,6 +179,8 @@ const loading = ref(false);
|
||||
const songList = ref<any[]>([]);
|
||||
const listInfo = ref<any>(null);
|
||||
const canRemove = ref(false);
|
||||
const canCollect = ref(false);
|
||||
const isCollected = ref(false);
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = 40;
|
||||
@@ -135,6 +194,35 @@ const hasMore = ref(true); // 标记是否还有更多数据可加载
|
||||
const searchKeyword = ref(''); // 搜索关键词
|
||||
const isFullPlaylistLoaded = ref(false); // 标记完整播放列表是否已加载完成
|
||||
|
||||
// 添加搜索相关的状态和方法
|
||||
const isSearchVisible = ref(false);
|
||||
const isCompactLayout = ref(localStorage.getItem('musicListLayout') === 'compact'); // 默认使用紧凑布局
|
||||
|
||||
const showSearch = () => {
|
||||
isSearchVisible.value = true;
|
||||
// 添加一个小延迟后聚焦搜索框
|
||||
nextTick(() => {
|
||||
const inputEl = document.querySelector('.search-container input');
|
||||
if (inputEl) {
|
||||
(inputEl as HTMLInputElement).focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeSearch = () => {
|
||||
isSearchVisible.value = false;
|
||||
searchKeyword.value = '';
|
||||
};
|
||||
|
||||
const handleSearchBlur = () => {
|
||||
// 如果搜索框为空,则在失焦时关闭搜索框
|
||||
if (!searchKeyword.value) {
|
||||
setTimeout(() => {
|
||||
isSearchVisible.value = false;
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算总数
|
||||
const total = computed(() => {
|
||||
if (listInfo.value?.trackIds) {
|
||||
@@ -146,6 +234,7 @@ const total = computed(() => {
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
initData();
|
||||
checkCollectionStatus();
|
||||
});
|
||||
|
||||
// 从 pinia 或路由参数获取数据
|
||||
@@ -639,6 +728,105 @@ watch(searchKeyword, () => {
|
||||
onUnmounted(() => {
|
||||
isPlaylistLoading.value = false;
|
||||
});
|
||||
|
||||
// 切换布局
|
||||
const toggleLayout = () => {
|
||||
isCompactLayout.value = !isCompactLayout.value;
|
||||
localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal');
|
||||
};
|
||||
|
||||
// 初始化歌单收藏状态
|
||||
const checkCollectionStatus = () => {
|
||||
// 只有歌单类型才能收藏
|
||||
if (route.query.type === 'playlist' && listInfo.value?.id) {
|
||||
canCollect.value = true;
|
||||
// 检查是否已收藏
|
||||
isCollected.value = listInfo.value.subscribed || false;
|
||||
} else {
|
||||
canCollect.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换收藏状态
|
||||
const toggleCollect = async () => {
|
||||
if (!listInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
loadingList.value = true;
|
||||
const tVal = isCollected.value ? 2 : 1; // 1:收藏, 2:取消收藏
|
||||
const response = await subscribePlaylist({
|
||||
t: tVal,
|
||||
id: listInfo.value.id
|
||||
});
|
||||
|
||||
// 假设API返回格式是 { data: { code: number, msg?: string } }
|
||||
const res = response.data;
|
||||
|
||||
if (res.code === 200) {
|
||||
isCollected.value = !isCollected.value;
|
||||
const msgKey = isCollected.value
|
||||
? 'comp.musicList.collectSuccess'
|
||||
: 'comp.musicList.cancelCollectSuccess';
|
||||
message.success(t(msgKey));
|
||||
// 更新歌单信息
|
||||
listInfo.value.subscribed = isCollected.value;
|
||||
} else {
|
||||
throw new Error(res.msg || t('comp.musicList.operationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏歌单失败:', error);
|
||||
message.error(t('comp.musicList.operationFailed'));
|
||||
} finally {
|
||||
loadingList.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放全部
|
||||
const handlePlayAll = () => {
|
||||
if (displayedSongs.value.length === 0) return;
|
||||
|
||||
// 如果有搜索关键词,只播放过滤后的歌曲
|
||||
if (searchKeyword.value) {
|
||||
playerStore.setPlayList(filteredSongs.value.map(formatSong));
|
||||
playerStore.setPlay(formatSong(filteredSongs.value[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则播放全部歌曲
|
||||
// 使用setPlayList设置播放列表
|
||||
playerStore.setPlayList(displayedSongs.value.map(formatSong));
|
||||
// 使用setPlay开始播放第一首
|
||||
playerStore.setPlay(formatSong(displayedSongs.value[0]));
|
||||
};
|
||||
|
||||
// 添加到播放列表末尾
|
||||
const addToPlaylist = () => {
|
||||
if (displayedSongs.value.length === 0) return;
|
||||
|
||||
// 获取当前播放列表
|
||||
const currentList = playerStore.playList;
|
||||
|
||||
// 如果有搜索关键词,只添加过滤后的歌曲
|
||||
const songsToAdd = searchKeyword.value
|
||||
? filteredSongs.value
|
||||
: displayedSongs.value;
|
||||
|
||||
// 添加歌曲到播放列表(避免重复添加)
|
||||
const newSongs = songsToAdd.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 }));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -701,15 +889,24 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@apply max-w-md;
|
||||
@apply max-w-md transition-all duration-300 ease-in-out;
|
||||
|
||||
&.search-expanded {
|
||||
@apply w-52;
|
||||
}
|
||||
|
||||
.search-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 hover:text-green-500;
|
||||
|
||||
.icon {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
@apply bg-light-200 dark:bg-dark-200;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.no-result {
|
||||
@@ -771,4 +968,35 @@ onUnmounted(() => {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-toggle .toggle-button,
|
||||
.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;
|
||||
}
|
||||
|
||||
&.collected {
|
||||
.icon {
|
||||
@apply text-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.hover-green:hover {
|
||||
.icon {
|
||||
@apply text-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user