Files
SPlayer/src/components/List/SongList.vue
2024-01-11 11:39:29 +08:00

797 lines
21 KiB
Vue

<!-- 歌曲列表 -->
<template>
<Transition name="fade" mode="out-in" @after-enter="checkHasPlaying">
<div v-if="data?.[0]?.id" class="song-list">
<div v-if="showTitle" class="song-list-header">
<n-text class="num" depth="3"> # </n-text>
<n-text :class="['info', { 'has-cover': data[0].cover && showCover }]" depth="3">
{{ type === "song" ? "歌曲" : "声音" }}
</n-text>
<n-text v-if="data[0].album && showAlbum" class="album hidden" depth="3"> 专辑 </n-text>
<n-text v-if="data[0].updateTime && type === 'dj'" class="update hidden" depth="3">
更新日期
</n-text>
<n-text v-if="type !== 'dj'" class="control" />
<n-text v-if="data[0].playCount && type === 'dj'" class="count hidden" depth="3">
播放量
</n-text>
<n-text v-if="data[0].duration" class="duration hidden" depth="3"> 时长 </n-text>
<n-text v-if="data[0].size" class="size hidden" depth="3"> 大小 </n-text>
</div>
<n-card
v-for="(item, index) in data.slice(
(pageNumber - 1) * loadSize,
(pageNumber - 1) * loadSize + loadSize,
)"
:id="'song-list-' + index"
:key="index"
:content-style="{
padding: '16px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}"
:class="music.getPlaySongData?.id === item?.id ? 'songs play' : 'songs'"
hoverable
@click="checkCanClick(data, item, songsIndex + index)"
@dblclick.stop="playSong(data, item, songsIndex + index)"
@contextmenu="
songListDropdownRef?.openDropdown($event, data, item, songsIndex + index, sourceId, type)
"
>
<!-- 序号 -->
<n-text v-if="music.getPlaySongData?.id !== item?.id" class="num" depth="3">
{{ songsIndex + index + 1 }}
</n-text>
<n-icon v-else class="num" size="22">
<SvgIcon icon="music-note" />
</n-icon>
<!-- 封面 -->
<div v-if="item.cover && showCover" class="cover">
<n-image
:src="item.coverSize.s"
class="cover-img"
preview-disabled
lazy
@load="
(e) => {
e.target.style.opacity = 1;
}
"
>
<template #placeholder>
<div class="cover-loading">
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="song" />
</div>
</template>
</n-image>
</div>
<!-- 信息 -->
<div class="info">
<div class="title">
<!-- 名称 -->
<!-- @click.stop="type !== 'dj' && !item.path ? router.push(`/song?id=${item.id}`) : null" -->
<n-text class="name" depth="2">
{{ item?.name || "未知曲目" }}
</n-text>
<!-- 特权 -->
<n-tag
v-if="showPrivilege && item.fee === 1 && userData.detail?.profile?.vipType !== 11"
:bordered="false"
type="error"
size="small"
round
>
VIP
</n-tag>
<n-tag
v-if="showPrivilege && item.fee === 4"
:bordered="false"
type="error"
size="small"
round
>
EP
</n-tag>
<!-- 云盘 -->
<n-tag
v-if="showPrivilege && item.pc"
:bordered="false"
class="cloud"
type="info"
size="small"
round
>
<template #icon>
<n-icon>
<SvgIcon icon="cloud" />
</n-icon>
</template>
</n-tag>
<!-- MV -->
<n-tag
v-if="item?.mv"
:bordered="false"
class="mv"
type="warning"
size="small"
round
@click.stop="router.push(`/videos-player?id=${item.mv}`)"
>
MV
</n-tag>
</div>
<!-- 歌手 -->
<div v-if="Array.isArray(item.artists)" class="artist">
<n-text
v-for="ar in item.artists"
:key="ar.id"
class="ar"
@click.stop="router.push(`/artist?id=${ar.id}`)"
@dblclick.stop
>
{{ ar.name }}
</n-text>
</div>
<div v-else-if="type === 'dj'" class="artist">
<n-text class="ar" @dblclick.stop> 电台节目 </n-text>
</div>
<div v-else class="artist">
<n-text class="ar" @dblclick.stop> {{ item.artists || "未知艺术家" }} </n-text>
</div>
<!-- 别名 -->
<n-text v-if="item.alia" class="alia" depth="3">{{ item.alia }}</n-text>
</div>
<!-- 专辑 -->
<template v-if="showAlbum && type !== 'dj'">
<n-text
v-if="item.album"
class="album hidden"
@click.stop="
typeof item.album === 'object' ? router.push(`/album?id=${item.album.id}`) : null
"
@dblclick.stop
>
{{ typeof item.album === "object" ? item.album?.name || "未知专辑" : item.album }}
</n-text>
<n-text v-else class="album hidden">未知专辑</n-text>
</template>
<!-- 操作 -->
<div v-if="type !== 'dj'" class="action">
<!-- 喜欢歌曲 -->
<n-icon
:depth="dataStore.getSongIsLike(item?.id) ? 0 : 3"
class="favorite"
size="20"
@click.stop="
dataStore.changeLikeList(item?.id, !dataStore.getSongIsLike(item?.id), item?.path)
"
@dblclick.stop
>
<SvgIcon
:icon="
dataStore.getSongIsLike(item?.id) ? 'favorite-rounded' : 'favorite-outline-rounded'
"
/>
</n-icon>
<!-- 更多操作 -->
<n-icon
class="more mobile"
depth="3"
size="20"
@click.stop="
songListDrawerRef?.drawerOpen(data, item, songsIndex + index, sourceId, type)
"
@dblclick.stop
>
<SvgIcon icon="more" />
</n-icon>
</div>
<!-- 更新日期 -->
<n-text v-if="type === 'dj' && item.updateTime" class="update hidden" depth="3">
{{ getTimestampTime(item.updateTime, false) }}
</n-text>
<!-- 播放量 -->
<n-text v-if="type === 'dj' && item.playCount" class="count hidden" depth="3">
{{ item.playCount }}次
</n-text>
<!-- 时长 -->
<n-text v-if="item.duration" class="duration hidden" depth="3">{{ item.duration }}</n-text>
<n-text v-else class="duration"> -- </n-text>
<!-- 大小 -->
<n-text v-if="item.size" class="size hidden" depth="3">{{ item.size }}M</n-text>
</n-card>
<!-- 分页 -->
<Pagination
v-if="data.length >= loadSize && showPagination"
:totalCount="data.length"
:pageNumber="pageNumber"
@pageNumberChange="pageNumberChange"
/>
<!-- 右键菜单 -->
<SongListDropdown
ref="songListDropdownRef"
@playSong="playSong"
@delCloudSong="delCloudSong"
@deletePlaylistSong="deletePlaylistSong"
@delLocalSong="delLocalSong"
/>
<!-- 移动端菜单 -->
<SongListDrawer
ref="songListDrawerRef"
@playSong="playSong"
@delCloudSong="delCloudSong"
@deletePlaylistSong="deletePlaylistSong"
@delLocalSong="delLocalSong"
/>
<!-- 定位歌曲 -->
<Transition name="shrink" mode="out-in">
<n-card
v-if="isHasPlayingDom"
:bordered="false"
:content-style="{
padding: '0px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}"
class="scroll-to-song"
@click="checkHasPlaying('scroll')"
>
<n-icon size="26">
<SvgIcon icon="location" />
</n-icon>
</n-card>
</Transition>
</div>
<!-- 空列表 -->
<n-empty
v-else-if="data === 'empty' || data?.[0] === 'empty'"
description="空空如也,怎么一首歌都没有啊"
style="margin-top: 60px"
size="large"
/>
<!-- 错误 -->
<n-empty
v-else-if="data === 'error' || data?.[0] === 'error'"
description="列表获取出错,请重试"
style="margin-top: 60px"
size="large"
/>
<!-- 加载动画 -->
<div v-else class="loading">
<n-skeleton :repeat="10" text />
</div>
</Transition>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { setCloudDel } from "@/api/cloud";
import { addSongToPlayList } from "@/api/playlist";
import { siteData, siteSettings, musicData, siteStatus } from "@/stores";
import { initPlayer, fadePlayOrPause, addSongToNext } from "@/utils/Player";
import { getTimestampTime } from "@/utils/timeTools";
const router = useRouter();
const music = musicData();
const dataStore = siteData();
const status = siteStatus();
const settings = siteSettings();
const { userData } = storeToRefs(dataStore);
const { loadSize, playSearch, useMusicCache } = storeToRefs(settings);
const { playList, playSongData, playSongSource } = storeToRefs(music);
const { playIndex, playMode, playHeartbeatMode, playLoading } = storeToRefs(status);
// eslint-disable-next-line no-unused-vars
const props = defineProps({
// 列表类型
type: {
type: String,
default: "song",
},
// 列表数据
data: {
type: [Array, String],
default: () => [],
},
// 列表 id
sourceId: {
type: [Number, String],
default: 0,
},
// 是否显示封面
showCover: {
type: Boolean,
default: true,
},
// 是否显示分页
showPagination: {
type: Boolean,
default: true,
},
// 是否显示特权
showPrivilege: {
type: Boolean,
default: true,
},
// 是否显示专辑
showAlbum: {
type: Boolean,
default: true,
},
// 是否显示表头
showTitle: {
type: Boolean,
default: true,
},
});
// 分页数据
const pageNumber = ref(1);
// 子组件
const songListDrawerRef = ref(null);
const songListDropdownRef = ref(null);
// 当前索引
const songsIndex = computed(() => {
const page = props.showPagination
? pageNumber.value
: Number(router.currentRoute.value.query?.page) || 1;
return (page - 1) * loadSize.value;
});
// 正在播放元素
const isHasPlayingDom = ref(null);
// 检查是否有正在播放的歌曲
const checkHasPlaying = (isScoll = null) => {
const songDom = document.querySelector(".songs.play");
// 滚动至元素
if (isScoll === "scroll" && songDom) {
songDom?.scrollIntoView({ behavior: "smooth", block: "center" });
}
isHasPlayingDom.value = songDom ? true : false;
};
// 播放歌曲
const playSong = async (data, song, index) => {
console.log(data, song, index);
// 若开启了缓存且正在加载
if (useMusicCache.value && playLoading.value) {
$message.warning("歌曲正在缓冲中,请稍后");
return false;
}
// 更改模式
playMode.value = props.type === "song" ? "normal" : "dj";
// 检查当前页面
const isPage = router.currentRoute.value.matched?.[0].path || null;
// 是否关闭心动模式
if (isPage !== "/like-songs") playHeartbeatMode.value = false;
// 是否为当前播放歌曲
if (music.getPlaySongData?.id === song?.id) {
// 继续播放
fadePlayOrPause();
} else {
// 若为特殊状态
if (
(isPage === "/search" && !playSearch.value) ||
isPage === "/history" ||
playHeartbeatMode.value
) {
console.log("仅播放当前歌曲");
addSongToNext(song, true);
} else {
// 添加播放列表
playList.value = data.slice();
// 更改播放索引
playIndex.value = index;
}
console.log("与当前播放歌曲不一致");
playSongData.value = song;
// 初始化播放器
await initPlayer(true);
}
// 附加来源
playSongSource.value = Number(props.sourceId);
// 检查是否有正在播放的歌曲
nextTick().then(() => checkHasPlaying());
};
// 页数变化
const pageNumberChange = (page) => {
pageNumber.value = page;
nextTick().then(() => {
const mainLayout = document.getElementById("main-layout");
mainLayout?.scrollIntoView({ behavior: "smooth" });
checkHasPlaying();
});
};
// 检查是否可执行双击
const checkCanClick = (data, item, index) => {
if (window.innerWidth > 700) return false;
playSong(data, item, index);
};
// 云盘歌曲删除
const delCloudSong = (data, song, index) => {
console.log(data, song, index);
$dialog.warning({
title: "确认删除",
content: `确认从云盘中删除 ${song.name}?该操作无法撤销!`,
positiveText: "删除",
negativeText: "取消",
onPositiveClick: async () => {
const result = await setCloudDel(song.id);
if (result.code == 200) {
data.splice(index, 1);
$message.success("删除成功");
} else {
$message.error("删除失败,请重试");
}
},
});
};
// 歌单歌曲删除
const deletePlaylistSong = (pid, song, data, index) => {
$dialog.warning({
title: "确认删除",
content: `确认从歌单中移除 ${song.name}?该操作无法撤销!`,
positiveText: "删除",
negativeText: "取消",
onPositiveClick: async () => {
const result = await addSongToPlayList(pid, song?.id, "del");
if (result.status === 200) {
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
$message.success("歌曲删除成功");
} else {
$message.error("歌曲删除失败,请重试");
}
},
});
};
// 本地歌曲删除
const delLocalSong = (data, song, index) => {
$dialog.warning({
title: "确认删除",
content: `确认从本地磁盘中删除 ${song.name}?该操作无法撤销!`,
positiveText: "删除",
negativeText: "取消",
onPositiveClick: async () => {
console.log(data, song, index);
const result = await electron.ipcRenderer.invoke("deleteFile", song?.path);
if (result) {
data.length === 1 ? data.splice(0, 1, "empty") : data.splice(index, 1);
$message.success("歌曲删除成功");
} else {
$message.error("歌曲删除失败,请重试");
}
},
});
};
// 监听歌曲变化
watch(
() => music.getPlaySongData?.id,
() => nextTick().then(() => checkHasPlaying()),
);
onMounted(() => {
checkHasPlaying();
});
onBeforeUnmount(() => {
isHasPlayingDom.value = false;
});
</script>
<style lang="scss" scoped>
.song-list {
width: 100%;
.song-list-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 12px;
padding: 0 16px;
* {
box-sizing: border-box;
}
.num {
width: 30px;
margin-right: 16px;
text-align: center;
}
.info,
.album {
flex: 1;
}
.has-cover {
margin-right: 66px;
}
.control {
width: 40px;
}
.update {
width: 80px;
text-align: center;
margin-right: auto;
}
.count {
width: 120px;
text-align: center;
}
.duration {
width: 50px;
text-align: center;
}
.size {
width: 80px;
padding-right: 10px;
text-align: right;
}
}
.songs {
border-radius: 8px;
margin-bottom: 12px;
transition:
transform 0.3s,
border-color 0.3s,
box-shadow 0.3s;
cursor: pointer;
.cover {
width: 50px;
height: 50px;
min-width: 50px;
border-radius: 8px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.cover-img {
width: 100%;
height: 100%;
z-index: 1;
transition:
filter 0.3s,
transform 0.3s;
:deep(img) {
width: 100%;
opacity: 0;
transition: opacity 0.35s ease-in-out;
}
}
}
.num {
width: 30px;
height: 30px;
min-width: 30px;
border-radius: 8px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-right: 20px;
.title {
display: flex;
align-items: center;
flex-direction: row;
.name {
font-size: 16px;
font-weight: bold;
transition: color 0.3s;
-webkit-line-clamp: 2;
&:hover {
color: var(--main-color);
}
}
.n-tag {
margin-left: 8px;
height: 18px;
&.cloud {
padding: 0 10px;
align-items: center;
justify-content: center;
:deep(.n-tag__icon) {
margin-right: 0;
width: 100%;
}
}
&.mv {
cursor: pointer;
}
}
}
.artist {
margin-top: 2px;
font-size: 13px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
word-break: break-all;
.ar {
display: inline-flex;
transition: opacity 0.3s;
opacity: 0.6;
cursor: pointer;
&::after {
content: "/";
margin: 0 4px;
}
&:last-child {
&::after {
display: none;
}
}
&:hover {
opacity: 0.8;
}
}
}
.alia {
margin-top: 2px;
font-size: 12px;
opacity: 0.8;
}
}
.album {
flex: 1;
padding-right: 20px;
transition: color 0.3s;
-webkit-line-clamp: 2;
&:hover {
color: var(--main-color);
}
}
.action {
width: 40px;
display: flex;
align-items: center;
justify-content: space-evenly;
.favorite {
padding-top: 1px;
transition: transform 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.15);
}
&:active {
transform: scale(1);
}
}
.more {
display: none;
}
}
.update {
width: 80px;
text-align: center;
}
.count {
width: 120px;
text-align: center;
}
.duration {
width: 50px;
text-align: center;
}
.size {
width: 80px;
text-align: right;
}
&.play {
background-color: var(--main-second-color);
border-color: var(--main-color);
a,
span,
.num {
color: var(--main-color) !important;
}
.artist {
.ar {
opacity: 0.8;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
}
.album {
opacity: 0.8;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
}
&:last-child {
margin-bottom: 0;
}
&:hover {
border-color: var(--main-color);
box-shadow:
0 1px 2px -2px var(--main-boxshadow-color),
0 3px 6px 0 var(--main-boxshadow-color),
0 5px 12px 4px var(--main-boxshadow-hover-color);
}
&:active {
transform: scale(0.995);
}
}
.scroll-to-song {
position: absolute;
width: 44px;
height: 44px;
right: calc(40px);
bottom: 100px;
border-radius: 50%;
background-color: var(--n-color-embedded-popover);
transition:
transform 0.3s,
opacity 0.3s;
cursor: pointer;
&:active {
transform: scale(0.9);
}
}
@media (max-width: 700px) {
.song-list-header,
.songs {
.hidden {
display: none;
}
}
.songs {
.num {
font-size: 12px;
width: 28px;
height: 28px;
min-width: 28px;
}
.info {
.title {
.name {
font-size: 15px;
}
}
.artist {
font-size: 12px;
}
}
.action {
width: 60px;
justify-content: flex-end;
.more {
display: inline-block;
margin-left: 12px;
}
}
}
}
}
.loading {
:deep(.n-skeleton) {
&:nth-of-type(1) {
margin-top: 0;
}
height: 80px;
margin-top: 12px;
border-radius: 8px;
}
}
</style>