修改左侧导航栏, 添加歌单页面

This commit is contained in:
alger
2021-09-29 15:26:13 +08:00
parent 102c17db2f
commit e108059773
12 changed files with 648 additions and 62 deletions

View File

@@ -7,7 +7,7 @@
<title>Vite App</title>
<link
rel="stylesheet"
href="//at.alicdn.com/t/font_2685283_qczwwfdwv9.css"
href="//at.alicdn.com/t/font_2685283_9pkmebnv585.css"
/>
<link rel="stylesheet" href="./public/css/animate.css" />
<style>

24
src/api/list.ts Normal file
View File

@@ -0,0 +1,24 @@
import request from "@/utils/request";
import { IList, IRecommendList } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
interface IListByTagParams {
tag: string;
before: number;
limit: number;
}
// 根据tag 获取歌单列表
export function getListByTag(params: IListByTagParams) {
return request.get<IList>("/top/playlist/highquality", { params: params });
}
// 获取推荐歌单
export function getRecommendList(limit: number = 30) {
return request.get<IRecommendList>("/personalized", { params: { limit } });
}
// 获取歌单详情
export function getListDetail(id: number | string) {
return request.get<IListDetail>("/playlist/detail", { params: { id } });
}

View File

@@ -54,7 +54,7 @@ const isPlaying = computed(() => {
})
// 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = (item: SongResult) => {
const playMusicEvent = (item: any) => {
store.commit("setPlay", item);
store.commit("setIsPlay", true);
};

View File

@@ -9,9 +9,12 @@
<search-bar />
<!-- 主页面路由 -->
<n-layout class="main-content bg-black" :native-scrollbar="false">
<keep-alive>
<router-view class="main-page"></router-view>
</keep-alive>
<router-view class="main-page" v-slot="{ Component }">
<keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>
</n-layout>
</div>
</div>
@@ -57,8 +60,7 @@ const menus = store.state.menus;
height: 834px;
}
&-page {
padding: 20px 0;
padding-bottom: 80px;
margin: 20px 0;
}
}
}

View File

@@ -8,10 +8,14 @@
</div>
</div>
<div class="app-menu-list">
<div class="app-menu-item" v-for="(item,index) in menus" :key="index">
<router-link class="app-menu-item-link" :to="item.href">
<i class="iconfont app-menu-item-icon" :style="iconStyle" :class="item.icon"></i>
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.text }}</span>
<div class="app-menu-item" v-for="(item,index) in menus">
<router-link class="app-menu-item-link" :to="item.path">
<i
class="iconfont app-menu-item-icon"
:style="iconStyle(index)"
:class="item.mate.icon"
></i>
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.mate.title }}</span>
</router-link>
</div>
</div>
@@ -20,15 +24,8 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from "@vue/runtime-core";
import type { PropType } from "vue";
interface AppMenuItem {
href: string;
icon: string;
text: string;
}
import { computed, onMounted, ref, watch } from "@vue/runtime-core";
import { useRoute } from "vue-router";
const props = defineProps({
isText: {
type: Boolean,
@@ -42,20 +39,32 @@ const props = defineProps({
type: String,
default: '#aaa'
},
selectColor: {
type: String,
default: '#10B981'
},
menus: {
type: Array as PropType<AppMenuItem[]>,
type: Array as any,
default: []
}
})
let iconStyle = ref({})
onMounted(() => {
// 初始化
iconStyle.value = {
fontSize: props.size,
color: props.color
}
const route = useRoute();
const path = ref(route.path);
watch(() => route.path, async newParams => {
console.log(newParams);
path.value = newParams
})
const iconStyle = (index: any) => {
let style = {
fontSize: props.size,
color: path.value === props.menus[index].path ? props.selectColor : props.color
}
return style
}
</script>
<style lang="scss" scoped>
@@ -73,7 +82,7 @@ onMounted(() => {
}
.app-menu-item-icon:hover {
color: #fff !important;
color: #10b981 !important;
transform: scale(1.05);
transition: 0.2s ease-in-out;
}

34
src/router/home.ts Normal file
View File

@@ -0,0 +1,34 @@
const layoutRouter = [
{
path: "/",
name: "home",
mate: {
keepAlive: true,
title: "首页",
icon: "icon-Home",
},
component: () => import("@/views/home/index.vue"),
},
{
path: "/search",
name: "search",
mate: {
title: "搜索",
keepAlive: true,
icon: "icon-Search",
},
component: () => import("@/views/search/index.vue"),
},
{
path: "/list",
name: "list",
mate: {
title: "歌单",
keepAlive: true,
icon: "icon-Paper",
},
component: () => import("@/views/list/index.vue"),
},
];
export default layoutRouter;

View File

@@ -1,23 +1,12 @@
import { createRouter, createWebHistory } from "vue-router";
import AppLayout from "@/layout/AppLayout.vue";
const layoutRouter = [
{
path: "",
name: "home",
component: () => import("@/views/home/index.vue"),
},
{
path: "/search",
name: "search",
component: () => import("@/views/search/index.vue"),
},
];
import homeRouter from "@/router/home";
const routes = [
{
path: "/",
component: AppLayout,
children: layoutRouter,
children: homeRouter,
},
];

View File

@@ -1,25 +1,9 @@
import { createStore } from "vuex";
import { SongResult } from "@/type/music";
import { getMusicUrl } from "@/api/music";
import homeRouter from "@/router/home";
let state = {
menus: [
{
href: "/",
icon: "icon-homefill",
text: "hello",
},
{
href: "/search",
icon: "icon-peoplefill",
text: "hello",
},
{
href: "/",
icon: "icon-videofill",
text: "hello",
},
],
menus: homeRouter,
play: false,
isPlay: false,
playMusic: {} as SongResult,

147
src/type/list.ts Normal file
View File

@@ -0,0 +1,147 @@
export interface IList {
playlists: Playlist[];
code: number;
more: boolean;
lasttime: number;
total: number;
}
interface Playlist {
name: string;
id: number;
trackNumberUpdateTime: number;
status: number;
userId: number;
createTime: number;
updateTime: number;
subscribedCount: number;
trackCount: number;
cloudTrackCount: number;
coverImgUrl: string;
coverImgId: number;
description: string;
tags: string[];
playCount: number;
trackUpdateTime: number;
specialType: number;
totalDuration: number;
creator: Creator;
tracks?: any;
subscribers: Subscriber[];
subscribed: boolean;
commentThreadId: string;
newImported: boolean;
adType: number;
highQuality: boolean;
privacy: number;
ordered: boolean;
anonimous: boolean;
coverStatus: number;
recommendInfo?: any;
shareCount: number;
coverImgId_str?: string;
commentCount: number;
copywriter: string;
tag: string;
}
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;
avatarImgIdStr: string;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgId_str?: string;
}
interface Creator {
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?: string[];
experts?: Expert;
djStatus: number;
vipType: number;
remarkName?: any;
authenticationTypes: number;
avatarDetail?: AvatarDetail;
avatarImgIdStr: string;
backgroundImgIdStr: string;
anchor: boolean;
avatarImgId_str?: string;
}
interface AvatarDetail {
userType: number;
identityLevel: number;
identityIconUrl: string;
}
interface Expert {
"2": string;
"1"?: string;
}
// 推荐歌单
export interface IRecommendList {
hasTaste: boolean;
code: number;
category: number;
result: IRecommendItem[];
}
export interface IRecommendItem {
id: number;
type: number;
name: string;
copywriter: string;
picUrl: string;
canDislike: boolean;
trackNumberUpdateTime: number;
playCount: number;
trackCount: number;
highQuality: boolean;
alg: string;
}

203
src/type/listDetail.ts Normal file
View File

@@ -0,0 +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;
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="main-page pb-20">
<div class="main-page">
<!-- 推荐歌手 -->
<recommend-singer />
<div class="main-content">
@@ -19,9 +19,12 @@ import PlaylistType from "@/components/PlaylistType.vue";
import RecommendSonglist from "@/components/RecommendSonglist.vue";
import RecommendAlbum from "@/components/RecommendAlbum.vue";
</script>
</script>
<style lang="scss" scoped>
.main-page {
@apply mt-4 pb-32;
}
.main-content {
@apply mt-6 flex;
}

191
src/views/list/index.vue Normal file
View File

@@ -0,0 +1,191 @@
<script lang="ts" setup>
import { getRecommendList, getListDetail } from '@/api/list'
import { computed, onMounted, ref } from 'vue';
import type { IRecommendList, IRecommendItem } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { setAnimationClass, setAnimationDelay } from "@/utils";
import SongItem from "@/components/common/SongItem.vue";
const recommendList = ref<IRecommendList>()
const showMusic = ref(false)
onMounted(async () => {
const { data } = await getRecommendList()
recommendList.value = data
})
const recommendItem = ref<IRecommendItem>()
const listDetail = ref<IListDetail>()
const selectRecommendItem = async (item: IRecommendItem) => {
const { data } = await getListDetail(item.id)
showMusic.value = true
recommendItem.value = item
listDetail.value = data
console.log(data);
}
const closeMusic = () => {
showMusic.value = false
}
const musicFullClass = computed(() => {
if (recommendItem.value) {
return setAnimationClass('animate__fadeInUp')
} else {
return setAnimationClass('animate__fadeOutDown')
}
})
// 格式化数字 千,万, 百万, 千万,亿
const formatNumber = (num: any) => {
num = num * 1
if (num < 10000) {
return num
}
if (num < 100000000) {
return (num / 10000).toFixed(1) + '万'
}
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
})
</script>
<template>
<div class="list-page">
<n-layout class="recommend" :native-scrollbar="false" @click="showMusic = false">
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">歌单推荐</div>
<div class="recommend-list">
<div
class="recommend-item"
v-for="(item,index) in recommendList?.result"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
@click.stop="selectRecommendItem(item)"
>
<div class="recommend-item-img">
<img :src="item.picUrl + '?param=200y200'" alt />
<div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i>
</div>
</div>
<div class="recommend-item-title">{{ item.name }}</div>
</div>
</div>
</n-layout>
<div class="music-page" v-show="showMusic" :class="musicFullClass">
<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__bounceInRight')"
:style="setAnimationDelay(index, 100)"
>
<SongItem :item="formatDetail(item)" />
</div>
</n-layout>
</div>
</div>
</template>
<style lang="scss" scoped>
.list-page {
position: relative;
}
.recommend {
width: 100%;
height: 800px;
background-color: #000000;
&-title {
@apply text-lg font-bold text-white py-4;
}
&-list {
@apply flex flex-wrap;
}
&-item {
width: 200px;
@apply mr-6 mb-4;
&-img {
@apply rounded-xl overflow-hidden relative;
&:hover img {
@apply hover:scale-110 transition-all duration-300 ease-in-out;
}
img {
width: 200px;
height: 200px;
}
.top {
@apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;
background-color: #00000088;
opacity: 0;
i {
font-size: 50px;
transition: all 0.5s ease-in-out;
opacity: 0;
}
&:hover {
@apply opacity-100;
}
&:hover i {
@apply transform scale-150 opacity-100;
}
.play-count {
position: absolute;
top: 10px;
left: 10px;
font-size: 14px;
}
}
}
&-title {
@apply p-2 text-sm text-white truncate;
}
}
}
.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>