mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-01 05:27:22 +08:00
✨ feat: 添加歌手详情抽屉
This commit is contained in:
+6
-1
@@ -5,4 +5,9 @@
|
|||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 添加下载管理 进度显示 播放下载的音乐
|
- 添加下载管理 进度显示 播放下载的音乐
|
||||||
- 添加缓存管理 清理缓存
|
- 添加缓存管理 清理缓存
|
||||||
- 优化下载格式问题 支持下载其他格式 ps:其实之前只是后缀名问题
|
- 优化下载格式问题 支持下载其他格式 ps:其实之前只是后缀名问题
|
||||||
|
|
||||||
|
## 咖啡☕️
|
||||||
|
| 微信 | | 支付宝 |
|
||||||
|
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
|
||||||
|
| <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/wechat.png" alt="WeChat QRcode" width=200>| | <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/alipay.png" alt="Wechat QRcode" width=200> |
|
||||||
+1
-1
@@ -60,7 +60,7 @@ app.whenReady().then(() => {
|
|||||||
initialize();
|
initialize();
|
||||||
|
|
||||||
// macOS 激活应用时的处理
|
// macOS 激活应用时的处理
|
||||||
app.on('activate', function () {
|
app.on('activate', () => {
|
||||||
if (mainWindow === null) initialize();
|
if (mainWindow === null) initialize();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
// 获取歌手详情
|
||||||
|
export const getArtistDetail = (id) => {
|
||||||
|
return request.get('/artist/detail', { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取歌手热门歌曲
|
||||||
|
export const getArtistTopSongs = (params) => {
|
||||||
|
return request.get('/artist/songs', {
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
order: 'hot'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取歌手专辑
|
||||||
|
export const getArtistAlbums = (params) => {
|
||||||
|
return request.get('/artist/album', { params });
|
||||||
|
};
|
||||||
Vendored
+2
@@ -15,6 +15,7 @@ declare module 'vue' {
|
|||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||||
@@ -29,6 +30,7 @@ declare module 'vue' {
|
|||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
mask-closable
|
mask-closable
|
||||||
:style="{ backgroundColor: 'transparent' }"
|
:style="{ backgroundColor: 'transparent' }"
|
||||||
:to="`#layout-main`"
|
:to="`#layout-main`"
|
||||||
|
:z-index="9998"
|
||||||
@mask-click="close"
|
@mask-click="close"
|
||||||
>
|
>
|
||||||
<div class="music-page">
|
<div class="music-page">
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
<div class="music-close">
|
<div class="music-close">
|
||||||
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
<i class="icon iconfont ri-close-line" @click="close"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-content">
|
<div class="music-content">
|
||||||
@@ -234,7 +235,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
@apply cursor-pointer text-gray-900 dark:text-white flex gap-2 items-center;
|
@apply cursor-pointer text-gray-500 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 flex gap-2 items-center transition;
|
||||||
.icon {
|
.icon {
|
||||||
@apply text-3xl;
|
@apply text-3xl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<n-drawer
|
||||||
|
v-model:show="modelValue"
|
||||||
|
:width="800"
|
||||||
|
placement="right"
|
||||||
|
:mask-closable="true"
|
||||||
|
:z-index="9997"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="artist-drawer">
|
||||||
|
<div class="close-btn">
|
||||||
|
<i class="ri-close-line" @click="modelValue = false"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 歌手信息头部 -->
|
||||||
|
<div class="artist-header">
|
||||||
|
<div class="artist-cover">
|
||||||
|
<n-image
|
||||||
|
:src="getImgUrl(artistInfo?.avatar, '300y300')"
|
||||||
|
class="w-48 h-48 rounded-2xl object-cover"
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="artist-info">
|
||||||
|
<h1 class="artist-name">{{ artistInfo?.name }}</h1>
|
||||||
|
<div v-if="artistInfo?.alias?.length" class="artist-alias">
|
||||||
|
{{ artistInfo.alias.join(' / ') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="artistInfo?.briefDesc" class="artist-desc">
|
||||||
|
{{ artistInfo.briefDesc }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签页切换 -->
|
||||||
|
<n-tabs v-model:value="activeTab" class="flex-1" type="line" animated>
|
||||||
|
<n-tab-pane name="songs" tab="热门歌曲">
|
||||||
|
<div ref="songListRef" class="songs-list">
|
||||||
|
<n-scrollbar style="max-height: 61vh" :size="5" @scroll="handleSongScroll">
|
||||||
|
<div class="song-list-content">
|
||||||
|
<song-item
|
||||||
|
v-for="song in songs"
|
||||||
|
:key="song.id"
|
||||||
|
:item="song"
|
||||||
|
:list="true"
|
||||||
|
@play="handlePlay"
|
||||||
|
/>
|
||||||
|
<div v-if="songLoading" class="loading-more">加载中...</div>
|
||||||
|
</div>
|
||||||
|
<play-bottom />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="albums" tab="专辑">
|
||||||
|
<div ref="albumListRef" class="albums-list">
|
||||||
|
<n-scrollbar style="max-height: 61vh" :size="5" @scroll="handleAlbumScroll">
|
||||||
|
<div class="albums-grid">
|
||||||
|
<search-item
|
||||||
|
v-for="album in albums"
|
||||||
|
:key="album.id"
|
||||||
|
shape="square"
|
||||||
|
:item="{
|
||||||
|
id: album.id,
|
||||||
|
picUrl: album.picUrl,
|
||||||
|
name: album.name,
|
||||||
|
desc: formatPublishTime(album.publishTime),
|
||||||
|
size: album.size,
|
||||||
|
type: '专辑'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div v-if="albumLoading" class="loading-more">加载中...</div>
|
||||||
|
</div>
|
||||||
|
<play-bottom />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="about" tab="艺人介绍">
|
||||||
|
<div class="artist-description">
|
||||||
|
<n-scrollbar style="max-height: 60vh">
|
||||||
|
<div class="description-content" v-html="artistInfo?.briefDesc"></div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</n-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDateFormat } from '@vueuse/core';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import { getArtistAlbums, getArtistDetail, getArtistTopSongs } from '@/api/artist';
|
||||||
|
import { getMusicDetail } from '@/api/music';
|
||||||
|
import SearchItem from '@/components/common/SearchItem.vue';
|
||||||
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
|
import { IArtist } from '@/type/artist';
|
||||||
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
|
import PlayBottom from './PlayBottom.vue';
|
||||||
|
|
||||||
|
const modelValue = defineModel<boolean>('show', { required: true });
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const activeTab = ref('songs');
|
||||||
|
const currentArtistId = ref<number>();
|
||||||
|
|
||||||
|
// 歌手信息
|
||||||
|
const artistInfo = ref<IArtist>();
|
||||||
|
const songs = ref<any[]>([]);
|
||||||
|
const albums = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const songLoading = ref(false);
|
||||||
|
const albumLoading = ref(false);
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const songPage = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 30,
|
||||||
|
hasMore: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const albumPage = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 30,
|
||||||
|
hasMore: true
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(modelValue, (newVal) => {
|
||||||
|
store.commit('setShowArtistDrawer', newVal);
|
||||||
|
});
|
||||||
|
const loading = ref(false);
|
||||||
|
// 加载歌手信息
|
||||||
|
const loadArtistInfo = async (id: number) => {
|
||||||
|
if (currentArtistId.value === id) return;
|
||||||
|
activeTab.value = 'songs';
|
||||||
|
loading.value = true;
|
||||||
|
currentArtistId.value = id;
|
||||||
|
try {
|
||||||
|
const info = await getArtistDetail(id);
|
||||||
|
if (info.data?.data?.artist) {
|
||||||
|
artistInfo.value = info.data.data.artist;
|
||||||
|
}
|
||||||
|
// 重置分页并加载初始数据
|
||||||
|
resetPagination();
|
||||||
|
await Promise.all([loadSongs(), loadAlbums()]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载歌手信息失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置分页
|
||||||
|
const resetPagination = () => {
|
||||||
|
songPage.value = {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 30,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
albumPage.value = {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 30,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
songs.value = [];
|
||||||
|
albums.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载歌曲
|
||||||
|
const loadSongs = async () => {
|
||||||
|
if (!currentArtistId.value || !songPage.value.hasMore || songLoading.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
songLoading.value = true;
|
||||||
|
const { page, pageSize } = songPage.value;
|
||||||
|
const res = await getArtistTopSongs({
|
||||||
|
id: currentArtistId.value,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (page - 1) * pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = res.data.songs.map((item) => item.id);
|
||||||
|
const songsDetail = await getMusicDetail(ids);
|
||||||
|
|
||||||
|
if (songsDetail.data?.songs) {
|
||||||
|
const newSongs = songsDetail.data.songs.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
picUrl: item.al.picUrl,
|
||||||
|
song: {
|
||||||
|
artists: item.ar,
|
||||||
|
name: item.name,
|
||||||
|
id: item.id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
songs.value = page === 1 ? newSongs : [...songs.value, ...newSongs];
|
||||||
|
songPage.value.hasMore = newSongs.length === pageSize;
|
||||||
|
songPage.value.page++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载歌曲失败:', error);
|
||||||
|
} finally {
|
||||||
|
songLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载专辑
|
||||||
|
const loadAlbums = async () => {
|
||||||
|
if (!currentArtistId.value || !albumPage.value.hasMore || albumLoading.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
albumLoading.value = true;
|
||||||
|
const { page, pageSize } = albumPage.value;
|
||||||
|
const res = await getArtistAlbums({
|
||||||
|
id: currentArtistId.value,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (page - 1) * pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data?.hotAlbums) {
|
||||||
|
const newAlbums = res.data.hotAlbums;
|
||||||
|
albums.value = page === 1 ? newAlbums : [...albums.value, ...newAlbums];
|
||||||
|
albumPage.value.hasMore = newAlbums.length === pageSize;
|
||||||
|
albumPage.value.page++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载专辑失败:', error);
|
||||||
|
} finally {
|
||||||
|
albumLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理滚动加载
|
||||||
|
const handleSongScroll = (e: { target: any }) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||||
|
loadSongs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAlbumScroll = (e: { target: any }) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化发布时间
|
||||||
|
const formatPublishTime = (time: number) => {
|
||||||
|
return useDateFormat(time, 'YYYY-MM-DD').value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
store.commit(
|
||||||
|
'setPlayList',
|
||||||
|
songs.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
picUrl: item.al.picUrl
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
loadArtistInfo
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.artist-drawer {
|
||||||
|
@apply h-full bg-light dark:bg-dark px-6 overflow-hidden flex flex-col;
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
@apply absolute top-4 right-4 text-gray-500 dark:text-gray-400 hover:text-green-500 text-2xl cursor-pointer p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-header {
|
||||||
|
@apply flex gap-6 pt-6;
|
||||||
|
|
||||||
|
.artist-info {
|
||||||
|
@apply flex-1;
|
||||||
|
|
||||||
|
.artist-name {
|
||||||
|
@apply text-4xl font-bold mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-alias {
|
||||||
|
@apply text-gray-500 dark:text-gray-400 mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-desc {
|
||||||
|
@apply text-sm text-gray-600 dark:text-gray-300 line-clamp-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.albums-grid {
|
||||||
|
@apply grid gap-4 grid-cols-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
@apply text-center py-4 text-gray-500 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-description {
|
||||||
|
.description-content {
|
||||||
|
@apply text-sm leading-relaxed whitespace-pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { setAnimationClass } from '@/utils';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
showPop: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showClose: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const musicFullClass = computed(() => {
|
|
||||||
if (props.showPop) {
|
|
||||||
return setAnimationClass('animate__fadeInUp');
|
|
||||||
}
|
|
||||||
return setAnimationClass('animate__fadeOutDown');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
|
|
||||||
<i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
|
|
||||||
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.pop-page {
|
|
||||||
height: 800px;
|
|
||||||
@apply absolute top-4 left-0 w-full;
|
|
||||||
background-color: #000000f0;
|
|
||||||
.close {
|
|
||||||
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-item" :class="item.type" @click="handleClick">
|
<div class="search-item" :class="[item.type, shape]" @click="handleClick">
|
||||||
<div class="search-item-img">
|
<div class="search-item-img">
|
||||||
<n-image
|
<n-image
|
||||||
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')"
|
class="w-full h-full"
|
||||||
|
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '200y200')"
|
||||||
lazy
|
lazy
|
||||||
preview-disabled
|
preview-disabled
|
||||||
/>
|
/>
|
||||||
@@ -15,6 +16,11 @@
|
|||||||
<p class="search-item-artist">{{ item.desc }}</p>
|
<p class="search-item-artist">{{ item.desc }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.type === '专辑'" class="search-item-size">
|
||||||
|
<i class="ri-music-2-line"></i>
|
||||||
|
<span>{{ item.size }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<music-list
|
<music-list
|
||||||
v-if="['专辑', 'playlist'].includes(item.type)"
|
v-if="['专辑', 'playlist'].includes(item.type)"
|
||||||
v-model:show="showPop"
|
v-model:show="showPop"
|
||||||
@@ -43,15 +49,21 @@ import { getImgUrl } from '@/utils';
|
|||||||
|
|
||||||
import MusicList from '../MusicList.vue';
|
import MusicList from '../MusicList.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
item: {
|
defineProps<{
|
||||||
picUrl: string;
|
shape?: 'square' | 'rectangle';
|
||||||
name: string;
|
item: {
|
||||||
desc: string;
|
picUrl: string;
|
||||||
type: string;
|
name: string;
|
||||||
[key: string]: any;
|
desc: string;
|
||||||
};
|
type: string;
|
||||||
}>();
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const songList = ref<any[]>([]);
|
const songList = ref<any[]>([]);
|
||||||
|
|
||||||
@@ -104,11 +116,45 @@ const handleClick = async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.search-item {
|
.search-item {
|
||||||
@apply rounded-3xl p-3 flex items-center hover:bg-light-200 dark:hover:bg-gray-800 transition cursor-pointer;
|
@apply rounded-lg p-0 flex items-center hover:bg-transparent transition cursor-pointer border-none;
|
||||||
margin: 0 10px;
|
|
||||||
.search-item-img {
|
&.square {
|
||||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
@apply flex-col relative;
|
||||||
|
|
||||||
|
.search-item-img {
|
||||||
|
@apply w-full aspect-square mb-2 mr-0 rounded-lg overflow-hidden hover:shadow-xl transition-all duration-300 shadow-sm shadow-black/20 dark:shadow-white/20;
|
||||||
|
img {
|
||||||
|
@apply object-cover w-full h-full transition-transform duration-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-item-info {
|
||||||
|
@apply w-full text-left px-0;
|
||||||
|
|
||||||
|
.search-item-name {
|
||||||
|
@apply truncate mb-1 font-medium text-base text-gray-800 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-item-artist {
|
||||||
|
@apply truncate text-sm text-gray-500 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-item-size {
|
||||||
|
@apply absolute top-2 right-2 text-xs text-white px-2 py-1 rounded-full bg-black/30 backdrop-blur-sm;
|
||||||
|
i {
|
||||||
|
@apply text-xs;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.rectangle {
|
||||||
|
@apply hover:bg-light-200 dark:hover:bg-dark-200 p-3;
|
||||||
|
.search-item-img {
|
||||||
|
@apply w-12 h-12 mr-4 rounded-lg overflow-hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search-item-info {
|
.search-item-info {
|
||||||
@apply flex-1 overflow-hidden;
|
@apply flex-1 overflow-hidden;
|
||||||
&-name {
|
&-name {
|
||||||
@@ -138,4 +184,8 @@ const handleClick = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-item-size {
|
||||||
|
@apply flex items-center gap-2 text-gray-400;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,10 +25,14 @@
|
|||||||
}}</n-ellipsis>
|
}}</n-ellipsis>
|
||||||
<div class="song-item-content-divider">-</div>
|
<div class="song-item-content-divider">-</div>
|
||||||
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
||||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
<template v-for="(artist, index) in artists" :key="index">
|
||||||
>{{ artists.name
|
<span
|
||||||
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</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>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -37,12 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="song-item-content-name">
|
<div class="song-item-content-name">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||||
<span
|
<template v-for="(artist, index) in artists" :key="index">
|
||||||
v-for="(artists, artistsindex) in item.ar || item.song.artists"
|
<span
|
||||||
:key="artistsindex"
|
class="cursor-pointer hover:text-green-500"
|
||||||
>{{ artists.name
|
@click.stop="handleArtistClick(artist.id)"
|
||||||
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
>{{ artist.name }}</span
|
||||||
>
|
>
|
||||||
|
<span v-if="index < artists.length - 1"> / </span>
|
||||||
|
</template>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -266,6 +272,15 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
const toggleSelect = () => {
|
const toggleSelect = () => {
|
||||||
emits('select', props.item.id, !props.selected);
|
emits('select', props.item.id, !props.selected);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleArtistClick = (id: number) => {
|
||||||
|
store.commit('setCurrentArtistId', id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取歌手列表(最多显示5个)
|
||||||
|
const artists = computed(() => {
|
||||||
|
return (props.item.ar || props.item.song?.artists)?.slice(0, 4) || [];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -31,11 +31,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<install-app-modal v-if="!isElectron"></install-app-modal>
|
<install-app-modal v-if="!isElectron"></install-app-modal>
|
||||||
<update-modal v-if="isElectron" />
|
<update-modal v-if="isElectron" />
|
||||||
|
<artist-drawer ref="artistDrawerRef" :show="artistDrawerShow" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
@@ -61,6 +62,8 @@ const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
|
|||||||
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
|
||||||
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
|
||||||
|
|
||||||
|
const ArtistDrawer = defineAsyncComponent(() => import('@/components/common/ArtistDrawer.vue'));
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const isPlay = computed(() => store.state.isPlay as boolean);
|
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||||
@@ -71,6 +74,25 @@ onMounted(() => {
|
|||||||
store.dispatch('initializeSettings');
|
store.dispatch('initializeSettings');
|
||||||
store.dispatch('initializeTheme');
|
store.dispatch('initializeTheme');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const artistDrawerRef = ref<InstanceType<typeof ArtistDrawer>>();
|
||||||
|
const artistDrawerShow = computed({
|
||||||
|
get: () => store.state.showArtistDrawer,
|
||||||
|
set: (val) => store.commit('setShowArtistDrawer', val)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听歌手ID变化
|
||||||
|
watch(
|
||||||
|
() => store.state.currentArtistId,
|
||||||
|
(newId) => {
|
||||||
|
if (newId) {
|
||||||
|
artistDrawerShow.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
artistDrawerRef.value?.loadArtistInfo(newId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -22,9 +22,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="music-content-name">{{ playMusic.name }}</div>
|
<div class="music-content-name">{{ playMusic.name }}</div>
|
||||||
<div class="music-content-singer">
|
<div class="music-content-singer">
|
||||||
<span v-for="(item, index) in playMusic.ar || playMusic.song.artists" :key="index">
|
<span
|
||||||
{{ item.name
|
v-for="(item, index) in playMusic.ar || playMusic.song.artists"
|
||||||
}}{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
:key="index"
|
||||||
|
class="cursor-pointer hover:text-green-500"
|
||||||
|
@click="handleArtistClick(item.id)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue';
|
import { onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
lrcArray,
|
lrcArray,
|
||||||
@@ -219,6 +225,12 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const handleArtistClick = (id: number) => {
|
||||||
|
props.musicFull = false;
|
||||||
|
store.commit('setCurrentArtistId', id);
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
lrcScroll
|
lrcScroll
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,11 +55,12 @@
|
|||||||
<span
|
<span
|
||||||
v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists"
|
v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists"
|
||||||
:key="artistsindex"
|
:key="artistsindex"
|
||||||
>{{ artists.name
|
class="cursor-pointer hover:text-green-500"
|
||||||
}}{{
|
@click="handleArtistClick(artists.id)"
|
||||||
artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : ''
|
|
||||||
}}</span
|
|
||||||
>
|
>
|
||||||
|
{{ artists.name
|
||||||
|
}}{{ artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
||||||
|
</span>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,6 +296,9 @@ const musicFullVisible = ref(false);
|
|||||||
const setMusicFull = () => {
|
const setMusicFull = () => {
|
||||||
musicFullVisible.value = !musicFullVisible.value;
|
musicFullVisible.value = !musicFullVisible.value;
|
||||||
store.commit('setMusicFull', musicFullVisible.value);
|
store.commit('setMusicFull', musicFullVisible.value);
|
||||||
|
if (musicFullVisible.value) {
|
||||||
|
store.commit('setShowArtistDrawer', false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const palyListRef = useTemplateRef('palyListRef');
|
const palyListRef = useTemplateRef('palyListRef');
|
||||||
@@ -322,6 +326,11 @@ const toggleFavorite = async (e: Event) => {
|
|||||||
const openLyricWindow = () => {
|
const openLyricWindow = () => {
|
||||||
openLyric();
|
openLyric();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleArtistClick = (id: number) => {
|
||||||
|
musicFullVisible.value = false;
|
||||||
|
store.commit('setCurrentArtistId', id);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const drag = (event: MouseEvent) => {
|
|||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
@apply flex justify-between px-6 py-2 select-none relative;
|
@apply flex justify-between px-6 py-2 select-none relative;
|
||||||
@apply text-dark dark:text-white;
|
@apply text-dark dark:text-white;
|
||||||
z-index: 9999999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
#buttons {
|
#buttons {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
|||||||
return item ? JSON.parse(item) : defaultValue;
|
return item ? JSON.parse(item) : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export interface State {
|
||||||
menus: any[];
|
menus: any[];
|
||||||
play: boolean;
|
play: boolean;
|
||||||
isPlay: boolean;
|
isPlay: boolean;
|
||||||
@@ -35,6 +35,8 @@ interface State {
|
|||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
musicFull: boolean;
|
musicFull: boolean;
|
||||||
showUpdateModal: boolean;
|
showUpdateModal: boolean;
|
||||||
|
showArtistDrawer: boolean;
|
||||||
|
currentArtistId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@@ -55,7 +57,9 @@ const state: State = {
|
|||||||
playMode: getLocalStorageItem('playMode', 0),
|
playMode: getLocalStorageItem('playMode', 0),
|
||||||
theme: getCurrentTheme(),
|
theme: getCurrentTheme(),
|
||||||
musicFull: false,
|
musicFull: false,
|
||||||
showUpdateModal: false
|
showUpdateModal: false,
|
||||||
|
showArtistDrawer: false,
|
||||||
|
currentArtistId: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||||
@@ -147,6 +151,15 @@ const mutations = {
|
|||||||
state.user = null;
|
state.user = null;
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
},
|
||||||
|
setShowArtistDrawer(state, show: boolean) {
|
||||||
|
state.showArtistDrawer = show;
|
||||||
|
if (!show) {
|
||||||
|
state.currentArtistId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentArtistId(state, id: number) {
|
||||||
|
state.currentArtistId = id;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,6 +210,9 @@ const actions = {
|
|||||||
|
|
||||||
// 更新本地存储
|
// 更新本地存储
|
||||||
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
||||||
|
},
|
||||||
|
showArtist({ commit }, id: number) {
|
||||||
|
commit('setCurrentArtistId', id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
export interface IArtistDetail {
|
||||||
|
videoCount: number;
|
||||||
|
vipRights: VipRights;
|
||||||
|
identify: Identify;
|
||||||
|
artist: IArtist;
|
||||||
|
blacklist: boolean;
|
||||||
|
preferShow: number;
|
||||||
|
showPriMsg: boolean;
|
||||||
|
secondaryExpertIdentiy: SecondaryExpertIdentiy[];
|
||||||
|
eventCount: number;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
backgroundUrl: string;
|
||||||
|
birthday: number;
|
||||||
|
detailDescription: string;
|
||||||
|
authenticated: boolean;
|
||||||
|
gender: number;
|
||||||
|
city: number;
|
||||||
|
signature: null;
|
||||||
|
description: string;
|
||||||
|
remarkName: null;
|
||||||
|
shortUserName: string;
|
||||||
|
accountStatus: number;
|
||||||
|
locationStatus: number;
|
||||||
|
avatarImgId: number;
|
||||||
|
defaultAvatar: boolean;
|
||||||
|
province: number;
|
||||||
|
nickname: string;
|
||||||
|
expertTags: null;
|
||||||
|
djStatus: number;
|
||||||
|
avatarUrl: string;
|
||||||
|
accountType: number;
|
||||||
|
authStatus: number;
|
||||||
|
vipType: number;
|
||||||
|
userName: string;
|
||||||
|
followed: boolean;
|
||||||
|
userId: number;
|
||||||
|
lastLoginIP: string;
|
||||||
|
lastLoginTime: number;
|
||||||
|
authenticationTypes: number;
|
||||||
|
mutual: boolean;
|
||||||
|
createTime: number;
|
||||||
|
anchor: boolean;
|
||||||
|
authority: number;
|
||||||
|
backgroundImgId: number;
|
||||||
|
userType: number;
|
||||||
|
experts: null;
|
||||||
|
avatarDetail: AvatarDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvatarDetail {
|
||||||
|
userType: number;
|
||||||
|
identityLevel: number;
|
||||||
|
identityIconUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecondaryExpertIdentiy {
|
||||||
|
expertIdentiyId: number;
|
||||||
|
expertIdentiyName: string;
|
||||||
|
expertIdentiyCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IArtist {
|
||||||
|
id: number;
|
||||||
|
cover: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
transNames: any[];
|
||||||
|
alias: any[];
|
||||||
|
identities: any[];
|
||||||
|
identifyTag: string[];
|
||||||
|
briefDesc: string;
|
||||||
|
rank: Rank;
|
||||||
|
albumSize: number;
|
||||||
|
musicSize: number;
|
||||||
|
mvSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rank {
|
||||||
|
rank: number;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Identify {
|
||||||
|
imageUrl: string;
|
||||||
|
imageDesc: string;
|
||||||
|
actionUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VipRights {
|
||||||
|
rightsInfoDetailDtoList: RightsInfoDetailDtoList[];
|
||||||
|
oldProtocol: boolean;
|
||||||
|
redVipAnnualCount: number;
|
||||||
|
redVipLevel: number;
|
||||||
|
now: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RightsInfoDetailDtoList {
|
||||||
|
vipCode: number;
|
||||||
|
expireTime: number;
|
||||||
|
iconUrl: null;
|
||||||
|
dynamicIconUrl: null;
|
||||||
|
vipLevel: number;
|
||||||
|
signIap: boolean;
|
||||||
|
signDeduct: boolean;
|
||||||
|
signIapDeduct: boolean;
|
||||||
|
sign: boolean;
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<n-layout
|
<n-layout
|
||||||
v-if="isMobile ? searchDetail : true"
|
v-if="isMobile ? searchDetail : true"
|
||||||
class="search-list"
|
class="search-list"
|
||||||
:class="setAnimationClass('animate__fadeInUp')"
|
:class="setAnimationClass('animate__fadeInDown')"
|
||||||
:native-scrollbar="false"
|
:native-scrollbar="false"
|
||||||
@scroll="handleScroll"
|
@scroll="handleScroll"
|
||||||
>
|
>
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(item, index) in list"
|
v-for="(item, index) in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
class="mb-3"
|
||||||
:class="setAnimationClass('animate__bounceInRight')"
|
:class="setAnimationClass('animate__bounceInRight')"
|
||||||
:style="setAnimationDelay(index, 50)"
|
:style="setAnimationDelay(index, 50)"
|
||||||
>
|
>
|
||||||
@@ -83,8 +84,8 @@
|
|||||||
<n-tag
|
<n-tag
|
||||||
v-for="(item, index) in searchHistory"
|
v-for="(item, index) in searchHistory"
|
||||||
:key="index"
|
:key="index"
|
||||||
:class="setAnimationClass('animate__bounceInLeft')"
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
:style="setAnimationDelay(index, 10)"
|
:style="setAnimationDelay(index, 50)"
|
||||||
class="search-history-item"
|
class="search-history-item"
|
||||||
round
|
round
|
||||||
closable
|
closable
|
||||||
@@ -362,6 +363,7 @@ const handleSearchHistory = (keyword: string) => {
|
|||||||
@apply flex-1 rounded-xl;
|
@apply flex-1 rounded-xl;
|
||||||
@apply bg-light-100 dark:bg-dark-100;
|
@apply bg-light-100 dark:bg-dark-100;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
|
||||||
&-box {
|
&-box {
|
||||||
@apply pb-28;
|
@apply pb-28;
|
||||||
|
|||||||
Reference in New Issue
Block a user