🦄 refactor(MusicList): 重构播放列表组件

This commit is contained in:
alger
2023-12-21 11:26:51 +08:00
parent 7e6788a057
commit 73c915d184
6 changed files with 316 additions and 360 deletions

View File

@@ -0,0 +1,77 @@
<template>
<n-drawer :show="show" height="70vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
<div class="music-page">
<i class="iconfont icon-icon_error music-close" @click="close"></i>
<div class="music-title">{{ musicList?.name }}</div>
<!-- 歌单歌曲列表 -->
<div class="music-list">
<n-scrollbar >
<div v-for="(item, index) in musicList?.tracks" :key="item.id" :class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)">
<SongItem :item="formatDetail(item)" @play="handlePlay" />
</div>
<PlayBottom/>
</n-scrollbar>
</div>
</div>
</n-drawer>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
import { Playlist } from '@/type/listDetail';
import { setAnimationClass, setAnimationDelay } from "@/utils";
import SongItem from "@/components/common/SongItem.vue";
import PlayBottom from './common/PlayBottom.vue';
const store = useStore()
const props = defineProps<{
show: boolean;
musicList: Playlist;
}>()
const emit = defineEmits(['update:show'])
const formatDetail = computed(() => (detail: any) => {
let song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
}
detail.song = song
detail.picUrl = detail.al.picUrl
return detail
})
const handlePlay = (item: any) => {
const tracks = props.musicList?.tracks || []
const musicIndex = (tracks.findIndex((music: any) => music.id == item.id) || 0)
store.commit('setPlayList', tracks.slice(musicIndex))
}
const close = () => {
emit('update:show', false)
}
</script>
<style scoped lang="scss">
.music {
&-page {
@apply px-8 w-full h-full bg-black bg-opacity-75 rounded-t-2xl;
backdrop-filter: blur(20px);
}
&-title {
@apply text-lg font-bold text-white p-4;
}
&-close {
@apply absolute top-4 right-8 cursor-pointer text-white text-3xl;
}
&-list {
height: calc(100% - 60px);
}
}
</style>

View File

@@ -0,0 +1,16 @@
<template>
<div class="bottom" v-if="isPlay"></div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore()
const isPlay = computed(() => store.state.isPlay as boolean)
</script>
<style lang="scss" scoped>
.bottom{
@apply h-28;
}
</style>

View File

@@ -67,9 +67,8 @@ const menus = store.state.menus;
@apply pr-6 flex-1 box-border;
height: 100vh;
&-content {
@apply rounded-2xl;
@apply rounded-2xl pb-28 box-border;
height: calc(100vh - 60px);
margin-bottom: 90px;
}
&-page {
margin: 20px 0;

View File

@@ -1,203 +1,203 @@
export interface IListDetail {
code: number;
relatedVideos?: any;
playlist: Playlist;
urls?: any;
privileges: Privilege[];
sharedPrivilege?: any;
resEntrance?: any;
}
interface Privilege {
id: number;
fee: number;
payed: number;
realPayed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
pc?: any;
toast: boolean;
flag: number;
paidBigBang: boolean;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
interface Playlist {
id: number;
name: string;
coverImgId: number;
coverImgUrl: string;
coverImgId_str: string;
adType: number;
userId: number;
createTime: number;
status: number;
opRecommend: boolean;
highQuality: boolean;
newImported: boolean;
updateTime: number;
trackCount: number;
specialType: number;
privacy: number;
trackUpdateTime: number;
commentThreadId: string;
playCount: number;
trackNumberUpdateTime: number;
subscribedCount: number;
cloudTrackCount: number;
ordered: boolean;
description: string;
tags: string[];
updateFrequency?: any;
backgroundCoverId: number;
backgroundCoverUrl?: any;
titleImage: number;
titleImageUrl?: any;
englishTitle?: any;
officialPlaylistType?: any;
subscribers: Subscriber[];
subscribed: boolean;
creator: Subscriber;
tracks: Track[];
videoIds?: any;
videos?: any;
trackIds: TrackId[];
shareCount: number;
commentCount: number;
remixVideo?: any;
sharedUsers?: any;
historySharedUsers?: any;
}
interface TrackId {
id: number;
v: number;
t: number;
at: number;
alg?: any;
uid: number;
rcmdReason: string;
}
interface Track {
name: string;
id: number;
pst: number;
t: number;
ar: Ar[];
alia: string[];
pop: number;
st: number;
rt?: string;
fee: number;
v: number;
crbt?: any;
cf: string;
al: Al;
dt: number;
h: H;
m: H;
l?: H;
a?: any;
cd: string;
no: number;
rtUrl?: any;
ftype: number;
rtUrls: any[];
djId: number;
copyright: number;
s_id: number;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
mst: number;
cp: number;
mv: number;
rtype: number;
rurl?: any;
publishTime: number;
tns?: string[];
}
interface H {
br: number;
fid: number;
size: number;
vd: number;
}
interface Al {
id: number;
name: string;
picUrl: string;
tns: any[];
pic_str?: string;
pic: number;
}
interface Ar {
id: number;
name: string;
tns: any[];
alias: any[];
}
interface Subscriber {
defaultAvatar: boolean;
province: number;
authStatus: number;
followed: boolean;
avatarUrl: string;
accountStatus: number;
gender: number;
city: number;
birthday: number;
userId: number;
userType: number;
nickname: string;
signature: string;
description: string;
detailDescription: string;
avatarImgId: number;
backgroundImgId: number;
backgroundUrl: string;
authority: number;
mutual: boolean;
expertTags?: any;
experts?: any;
djStatus: number;
vipType: number;
remarkName?: any;
authenticationTypes: number;
avatarDetail?: any;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgIdStr: string;
avatarImgId_str: string;
}
export interface IListDetail {
code: number;
relatedVideos?: any;
playlist: Playlist;
urls?: any;
privileges: Privilege[];
sharedPrivilege?: any;
resEntrance?: any;
}
interface Privilege {
id: number;
fee: number;
payed: number;
realPayed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
pc?: any;
toast: boolean;
flag: number;
paidBigBang: boolean;
preSell: boolean;
playMaxbr: number;
downloadMaxbr: number;
rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[];
}
interface ChargeInfoList {
rate: number;
chargeUrl?: any;
chargeMessage?: any;
chargeType: number;
}
interface FreeTrialPrivilege {
resConsumable: boolean;
userConsumable: boolean;
}
export interface Playlist {
id: number
name: string
coverImgId: number
coverImgUrl: string
coverImgId_str: string
adType: number
userId: number
createTime: number
status: number
opRecommend: boolean
highQuality: boolean
newImported: boolean
updateTime: number
trackCount: number
specialType: number
privacy: number
trackUpdateTime: number
commentThreadId: string
playCount: number
trackNumberUpdateTime: number
subscribedCount: number
cloudTrackCount: number
ordered: boolean
description: string
tags: string[]
updateFrequency?: any
backgroundCoverId: number
backgroundCoverUrl?: any
titleImage: number
titleImageUrl?: any
englishTitle?: any
officialPlaylistType?: any
subscribers: Subscriber[]
subscribed: boolean
creator: Subscriber
tracks: Track[]
videoIds?: any
videos?: any
trackIds: TrackId[]
shareCount: number
commentCount: number
remixVideo?: any
sharedUsers?: any
historySharedUsers?: any
}
interface TrackId {
id: number;
v: number;
t: number;
at: number;
alg?: any;
uid: number;
rcmdReason: string;
}
interface Track {
name: string;
id: number;
pst: number;
t: number;
ar: Ar[];
alia: string[];
pop: number;
st: number;
rt?: string;
fee: number;
v: number;
crbt?: any;
cf: string;
al: Al;
dt: number;
h: H;
m: H;
l?: H;
a?: any;
cd: string;
no: number;
rtUrl?: any;
ftype: number;
rtUrls: any[];
djId: number;
copyright: number;
s_id: number;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
mst: number;
cp: number;
mv: number;
rtype: number;
rurl?: any;
publishTime: number;
tns?: string[];
}
interface H {
br: number;
fid: number;
size: number;
vd: number;
}
interface Al {
id: number;
name: string;
picUrl: string;
tns: any[];
pic_str?: string;
pic: number;
}
interface Ar {
id: number;
name: string;
tns: any[];
alias: any[];
}
interface Subscriber {
defaultAvatar: boolean;
province: number;
authStatus: number;
followed: boolean;
avatarUrl: string;
accountStatus: number;
gender: number;
city: number;
birthday: number;
userId: number;
userType: number;
nickname: string;
signature: string;
description: string;
detailDescription: string;
avatarImgId: number;
backgroundImgId: number;
backgroundUrl: string;
authority: number;
mutual: boolean;
expertTags?: any;
experts?: any;
djStatus: number;
vipType: number;
remarkName?: any;
authenticationTypes: number;
avatarDetail?: any;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgIdStr: string;
avatarImgId_str: string;
}

View File

@@ -1,15 +1,13 @@
<script lang="ts" setup>
import { getRecommendList, getListDetail, getListByTag, getListByCat } from '@/api/list'
import { computed, onMounted, ref, watch } from 'vue';
import type { IRecommendList, IRecommendItem } from "@/type/list";
import { getRecommendList, getListDetail, getListByCat } from '@/api/list'
import { ref, watch } from 'vue';
import type { IRecommendItem } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import SongItem from "@/components/common/SongItem.vue";
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { useRoute } from 'vue-router';
import MusicList from "@/components/MusicList.vue";
import PlayBottom from '@/components/common/PlayBottom.vue';
const store = useStore();
const recommendList = ref()
const showMusic = ref(false)
@@ -21,9 +19,6 @@ const selectRecommendItem = async (item: IRecommendItem) => {
recommendItem.value = item
listDetail.value = data
}
const closeMusic = () => {
showMusic.value = false
}
const route = useRoute();
const listTitle = ref(route.query.type || "歌单列表");
@@ -58,15 +53,6 @@ watch(
}
)
const musicFullClass = computed(() => {
if (recommendItem.value) {
return setAnimationClass('animate__fadeInUp')
} else {
return setAnimationClass('animate__fadeOutDown')
}
})
// 格式化数字 千,万, 百万, 千万,亿
const formatNumber = (num: any) => {
num = num * 1
@@ -78,38 +64,16 @@ const formatNumber = (num: any) => {
}
return (num / 100000000).toFixed(1) + '亿'
}
const formatDetail = computed(() => (detail: any) => {
let song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
}
detail.song = song
detail.picUrl = detail.al.picUrl
return detail
})
const handlePlay = (item: any) => {
const list = listDetail.value?.playlist.tracks || []
console.log('list',list)
console.log('item',item)
const musicIndex = (list.findIndex((music: any) => music.id == item.id) || 0)
store.commit('setPlayList', list.slice(musicIndex))
}
</script>
<template>
<div class="list-page">
<!-- 歌单列表 -->
<n-scrollbar class="recommend" @click="showMusic = false" :size="100">
<div
<div
class="recommend-title"
:class="setAnimationClass('animate__bounceInLeft')"
>{{ listTitle }}</div>
<!-- 歌单列表 -->
<n-scrollbar class="recommend" @click="showMusic = false" :size="100">
<div class="recommend-list" v-if="recommendList">
<div
class="recommend-item"
@@ -133,25 +97,9 @@ const handlePlay = (item: any) => {
<div class="recommend-item-title">{{ item.name }}</div>
</div>
</div>
<PlayBottom/>
</n-scrollbar>
<transition name="musicPage">
<div class="music-page" v-if="showMusic">
<i class="iconfont icon-icon_error music-close" @click="closeMusic()"></i>
<div class="music-title">{{ recommendItem?.name }}</div>
<!-- 歌单歌曲列表 -->
<n-layout class="music-list" :native-scrollbar="false">
<div
v-for="(item, index) in listDetail?.playlist.tracks"
:key="item.id"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)"
>
<SongItem :item="formatDetail(item)" @play="handlePlay" />
</div>
</n-layout>
</div>
</transition>
<MusicList v-if="listDetail?.playlist" v-model:show="showMusic" :music-list="listDetail?.playlist" />
</div>
</template>
@@ -160,14 +108,6 @@ const handlePlay = (item: any) => {
@apply relative h-full w-full pt-4;
}
.musicPage-enter-active {
animation: fadeInUp 0.8s ease-in-out;
}
.musicPage-leave-active {
animation: fadeOutDown 0.8s ease-in-out;
}
.recommend {
@apply w-full h-full bg-none;
&-title {
@@ -218,29 +158,4 @@ const handlePlay = (item: any) => {
}
}
.music {
&-page {
// width: 100%;
// height: 70%;
// position: absolute;
// background-color: #000000f0;
// bottom: 0;
// left: 0;
// border-radius: 30px 30px 0 0;
// animation-duration: 300ms;
@apply w-full h-5/6 absolute bottom-0 left-0 bg-black rounded-t-3xl flex flex-col transition-all;
}
&-title {
@apply text-lg font-bold text-white p-4;
}
&-close {
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
}
&-list {
height: 594px;
background-color: #00000000;
}
}
</style>

View File

@@ -7,13 +7,12 @@ import { computed, ref } from "vue";
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import { getListDetail } from '@/api/list'
import SongItem from "@/components/common/SongItem.vue";
import MusicList from "@/components/MusicList.vue";
import type { Playlist } from '@/type/listDetail';
const store = useStore()
const router = useRouter()
const userDetail = ref<IUserDetail>()
const playList = ref<any[]>([])
const recordList = ref()
@@ -40,7 +39,7 @@ loadPage()
const isShowList = ref(false)
const list = ref()
const list = ref<Playlist>()
// 展示歌单
const showPlaylist = async (id: number) => {
const { data } = await getListDetail(id)
@@ -61,21 +60,12 @@ const formatDetail = computed(() => (detail: any) => {
return detail
})
const musicFullClass = computed(() => {
if (isShowList.value) {
return setAnimationClass('animate__fadeInUp')
} else {
return setAnimationClass('animate__fadeOutDown')
}
})
const handlePlay = (item: any) => {
const tracks = list.value?.tracks || []
const tracks = recordList.value || []
const musicIndex = (tracks.findIndex((music: any) => music.id == item.id) || 0)
store.commit('setPlayList', tracks.slice(musicIndex))
}
</script>
<template>
@@ -139,28 +129,12 @@ const handlePlay = (item: any) => {
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)"
>
<SongItem class="song-item" :item="formatDetail(item.song)" />
<SongItem class="song-item" :item="formatDetail(item.song)" @play="handlePlay"/>
<div class="play-count">{{ item.playCount }}</div>
</div>
</n-layout>
</div>
<transition name="musicPage">
<div class="music-page" v-if="isShowList">
<i class="iconfont icon-icon_error music-close" @click="isShowList = false"></i>
<div class="music-title">{{ list?.name }}</div>
<!-- 歌单歌曲列表 -->
<n-layout class="music-list" :native-scrollbar="false">
<div
v-for="(item, index) in list?.tracks"
:key="item.id"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)"
>
<SongItem :item="formatDetail(item)" @play="handlePlay"/>
</div>
</n-layout>
</div>
</transition>
<MusicList v-if="list" v-model:show="isShowList" :music-list="list" />
</div>
</template>
@@ -173,7 +147,7 @@ const handlePlay = (item: any) => {
animation: fadeOutDown 0.8s ease-in-out;
}
.user-page {
@apply flex;
@apply flex h-full;
.left {
max-width: 600px;
background-color: #0d0d0d;
@@ -204,9 +178,8 @@ const handlePlay = (item: any) => {
.right {
@apply flex-1 ml-4;
.record-list {
height: 750px;
background-color: #0d0d0d;
@apply rounded-2xl;
@apply rounded-2xl h-full;
.record-item {
@apply flex items-center px-4;
}
@@ -247,28 +220,4 @@ const handlePlay = (item: any) => {
}
}
.music {
&-page {
width: 100%;
height: 734px;
position: absolute;
background-color: #000000f0;
top: 100px;
left: 0;
border-radius: 30px 30px 0 0;
animation-duration: 300ms;
}
&-title {
@apply text-lg font-bold text-white p-4;
}
&-close {
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
}
&-list {
height: 594px;
background-color: #00000000;
}
}
</style>