首页 歌手推荐 歌单分类 最热音乐没写完

This commit is contained in:
algerkong
2021-07-20 15:29:20 +08:00
parent eed1211c6a
commit 778c2c1672
15 changed files with 553 additions and 32 deletions

View File

@@ -6,7 +6,13 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link rel="stylesheet" href="//at.alicdn.com/t/font_2685283_d0nzj20hrzk.css">
<link rel="stylesheet" href="//at.alicdn.com/t/font_2685283_m5ii4umo6k8.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<body>

13
package-lock.json generated
View File

@@ -465,6 +465,14 @@
"postcss-value-parser": "^4.1.0"
}
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npm.taobao.org/axios/download/axios-0.21.1.tgz",
"integrity": "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.2.tgz",
@@ -866,6 +874,11 @@
"to-regex-range": "^5.0.1"
}
},
"follow-redirects": {
"version": "1.14.1",
"resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz?cache=0&sync_timestamp=1620555300559&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.1.tgz",
"integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M="
},
"fs-extra": {
"version": "10.0.0",
"resolved": "https://registry.nlark.com/fs-extra/download/fs-extra-10.0.0.tgz",

View File

@@ -8,6 +8,7 @@
"dependencies": {
"@tailwindcss/postcss7-compat": "^2.2.4",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",
"postcss": "^7.0.36",
"sass": "^1.35.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",

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

@@ -0,0 +1,34 @@
import request from "@/utils/request";
import { IHotSinger } from "@/type/singer";
import { ISearchKeyword } from "@/type/search";
import { IPlayListSort } from "@/type/playlist";
import { IRecommendMusic } from "@/type/music";
interface IHotSingerParams {
offset: number;
limit: number;
}
interface IRecommendMusicParams {
limit: number;
}
// 获取热门歌手
export const getHotSinger = (params: IHotSingerParams) => {
return request.get<IHotSinger>("/top/artists", { params });
};
// 获取搜索推荐词
export const getSearchKeyword = () => {
return request.get<ISearchKeyword>("/search/default");
};
// 获取歌单分类
export const getPlaylistCategory = () => {
return request.get<IPlayListSort>("/playlist/catlist");
};
// 获取推荐音乐
export const getRecommendMusic = (params: IRecommendMusicParams) => {
return request.get<IRecommendMusic>("/personalized/newsong", { params });
};

View File

@@ -2,31 +2,80 @@
<div class="layout-page">
<div class="layout-main">
<app-menu class="menu" :menus="menus" />
<router-view class="main"></router-view>
<div class="main">
<div class="search-box flex">
<div class="search-box-input flex-1">
<n-input
size="large"
round
:placeholder="searchKeyword"
class="border border-gray-600"
>
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
</n-input>
</div>
<div class="user-box">
<n-popselect
v-model:value="value"
:options="options"
trigger="click"
size="small"
>
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-popselect>
<n-avatar
class="ml-2"
circle
size="large"
src="https://picsum.photos/200/300?random=1"
/>
</div>
</div>
<router-view></router-view>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from '@vue/reactivity';
import { useStore } from 'vuex';
import { AppMenu } from './components';
let menus = ref([
import { getSearchKeyword } from '@/api/home';
import { ref, onMounted } from 'vue';
const store = useStore();
const menus = store.state.menus;
const value = 'Drive My Car'
const options = [
{
href: '/',
icon: "icon-homefill",
text: "hello"
label: 'Girl',
value: 'Girl'
},
{
href: '/main',
icon: "icon-peoplefill",
text: "hello"
label: 'In My Life',
value: 'In My Life'
},
{
href: '/',
icon: "icon-videofill",
text: "hello"
},
])
label: 'Wait',
value: 'Wait'
}
]
const searchKeyword = ref<String>("搜索点什么吧...")
const loadSearchKeyword = async () => {
const { data } = await getSearchKeyword();
searchKeyword.value = data.data.showKeyword
}
onMounted(() => {
loadSearchKeyword()
})
</script>
<style lang="scss" scoped>
@@ -38,8 +87,10 @@ let menus = ref([
.layout-main {
@apply bg-black rounded-lg mb-10 text-white shadow-xl flex;
height: 800px;
width: 1400px;
height: 900px;
width: 1500px;
overflow: hidden;
min-width: 1500px;
.menu {
width: 90px;
}
@@ -47,5 +98,10 @@ let menus = ref([
@apply pt-6 pr-6 pb-6;
flex: 1;
}
.user-box {
@apply ml-6 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
background: #1a1a1a;
}
}
</style>

View File

@@ -4,12 +4,12 @@
<div class="app-menu">
<div class="app-menu-header">
<div class="app-menu-logo">
<img src="@/assets/logo.png" class="w-10 h-10" alt="logo" />
<img src="@/assets/logo.png" class="w-9 h-9 mt-2" alt="logo" />
</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 mb-4 mt-4" :to="item.href">
<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>
</router-link>
@@ -29,15 +29,15 @@ const props = defineProps({
},
size: {
type: String,
default: '30px'
default: '26px'
},
color: {
type: String,
default: '#fff'
default: '#aaa'
},
menus: {
type: Array,
default: () => []
default: []
}
})
@@ -61,7 +61,13 @@ onMounted(() => {
@apply flex items-center justify-center;
}
img {
color: #fff;
.app-menu-item-link {
@apply mb-6 mt-6;
}
.app-menu-item-icon:hover {
color: #fff !important;
transform: scale(1.05);
transition: 0.2s ease-in-out;
}
</style>

View File

@@ -10,7 +10,10 @@ import "./index.css";
import router from "@/router";
import store from "@/store";
const app = createApp(App);
app.use(router);
app.use(store);
app.use(naive);
app.mount("#app");

30
src/store/index.ts Normal file
View File

@@ -0,0 +1,30 @@
import { createStore } from "vuex";
let state = {
menus: [
{
href: "/",
icon: "icon-homefill",
text: "hello",
},
{
href: "/main",
icon: "icon-peoplefill",
text: "hello",
},
{
href: "/",
icon: "icon-videofill",
text: "hello",
},
],
};
let mutations = {};
const store = createStore({
state: state,
mutations: mutations,
});
export default store;

0
src/type/home.ts Normal file
View File

156
src/type/music.ts Normal file
View File

@@ -0,0 +1,156 @@
export interface IRecommendMusic {
code: number;
category: number;
result: Result[];
}
interface Result {
id: number;
type: number;
name: string;
copywriter?: any;
picUrl: string;
canDislike: boolean;
trackNumberUpdateTime?: any;
song: Song;
alg: string;
}
interface Song {
name: string;
id: number;
position: number;
alias: string[];
status: number;
fee: number;
copyrightId: number;
disc: string;
no: number;
artists: Artist[];
album: Album;
starred: boolean;
popularity: number;
score: number;
starredNum: number;
duration: number;
playedNum: number;
dayPlays: number;
hearTime: number;
ringtone: string;
crbt?: any;
audition?: any;
copyFrom: string;
commentThreadId: string;
rtUrl?: any;
ftype: number;
rtUrls: any[];
copyright: number;
transName?: any;
sign?: any;
mark: number;
originCoverType: number;
originSongSimpleData?: any;
single: number;
noCopyrightRcmd?: any;
rtype: number;
rurl?: any;
mvid: number;
bMusic: BMusic;
mp3Url?: any;
hMusic: BMusic;
mMusic: BMusic;
lMusic: BMusic;
exclusive: boolean;
privilege: Privilege;
}
interface Privilege {
id: number;
fee: number;
payed: number;
st: number;
pl: number;
dl: number;
sp: number;
cp: number;
subp: number;
cs: boolean;
maxbr: number;
fl: number;
toast: boolean;
flag: number;
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 BMusic {
name?: any;
id: number;
size: number;
extension: string;
sr: number;
dfsId: number;
bitrate: number;
playTime: number;
volumeDelta: number;
}
interface Album {
name: string;
id: number;
type: string;
size: number;
picId: number;
blurPicUrl: string;
companyId: number;
pic: number;
picUrl: string;
publishTime: number;
description: string;
tags: string;
company: string;
briefDesc: string;
artist: Artist;
songs: any[];
alias: string[];
status: number;
copyrightId: number;
commentThreadId: string;
artists: Artist[];
subType: string;
transName?: any;
onSale: boolean;
mark: number;
picId_str: string;
}
interface Artist {
name: string;
id: number;
picId: number;
img1v1Id: number;
briefDesc: string;
picUrl: string;
img1v1Url: string;
albumSize: number;
alias: any[];
trans: string;
musicSize: number;
topicPerson: number;
}

26
src/type/playlist.ts Normal file
View File

@@ -0,0 +1,26 @@
export interface IPlayListSort {
code: number;
all: SortAll;
sub: SortAll[];
categories: SortCategories;
}
interface SortCategories {
"0": string;
"1": string;
"2": string;
"3": string;
"4": string;
}
interface SortAll {
name: string;
resourceCount: number;
imgId: number;
imgUrl?: any;
type: number;
category: number;
resourceType: number;
hot: boolean;
activity: boolean;
}

16
src/type/search.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface ISearchKeyword {
code: number;
message?: any;
data: SearchKeywordData;
}
interface SearchKeywordData {
showKeyword: string;
realkeyword: string;
searchType: number;
action: number;
alg: string;
gap: number;
source?: any;
bizQueryInfo: string;
}

32
src/type/singer.ts Normal file
View File

@@ -0,0 +1,32 @@
export interface IHotSinger {
code: number;
more: boolean;
artists: Artist[];
}
interface Artist {
name: string;
id: number;
picId: number;
img1v1Id: number;
briefDesc: string;
picUrl: string;
img1v1Url: string;
albumSize: number;
alias: string[];
trans: string;
musicSize: number;
topicPerson: number;
showPrivateMsg?: any;
isSubed?: any;
accountId?: number;
picId_str?: string;
img1v1Id_str: string;
transNames?: string[];
followed: boolean;
mvSize?: any;
publishTime?: any;
identifyTag?: any;
alg?: any;
fansCount?: any;
}

8
src/utils/request.ts Normal file
View File

@@ -0,0 +1,8 @@
import axios from "axios";
const request = axios.create({
baseURL: "http://123.56.226.179:3000",
timeout: 10000,
});
export default request;

View File

@@ -1,22 +1,156 @@
<template>
<n-layout class="h-full bg-black" :native-scrollbar="false">
<div class="main-page">
<!--音乐搜索框 -->
<div class="search-box">
<div class="search-box-input">
<n-input size="large" round placeholder="搜索歌曲或歌手...">
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
</n-input>
<!-- 推荐歌手 -->
<div class="recommend-singer">
<div class="recommend-singer-list">
<div
class="recommend-singer-item relative"
v-for="(item,index) in hotSingerData?.artists"
:key="item.id"
>
<div :style="getStyle(item)" class="recommend-singer-item-bg"></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10"
>{{ item.musicSize }}</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play">
<i class="iconfont icon-playfill text-xl"></i>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="main-content">
<!-- 歌单分类列表 -->
<div class="play-list-type">
<div class="title">歌单分类</div>
<n-layout class="bg-black">
<template v-for="(item,index) in playlistCategory?.sub" :key="item.name">
<span
class="play-list-type-item animate__animated animate__bounceIn animate__repeat-1"
:style="getPlaylistTypeStyle(index <= 8 ? index : index - 8)"
v-if="isShowAllPlaylistCategory || index <= 8"
>{{ item.name }}</span>
</template>
<div
class="play-list-type-showall animate__animated animate__bounceIn animate__repeat-1"
:style="getPlaylistTypeStyle(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length + 30)"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
>{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}</div>
</n-layout>
</div>
<div class="recommend-music">
<div class="title">本周最热音乐</div>
<n-layout class="bg-black">
<img v-for="item in recommendMusic?.result" :src="item.picUrl" width="100" height="100" />
</n-layout>
</div>
</div>
</div>
</n-layout>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { getHotSinger, getPlaylistCategory, getRecommendMusic } from "@/api/home"
import type { IHotSinger } from "@/type/singer";
import type { IPlayListSort } from "@/type/playlist";
import type { IRecommendMusic } from "@/type/music";
// 歌手信息
const hotSingerData = ref<IHotSinger>()
// 歌单分类
const playlistCategory = ref<IPlayListSort>()
// 是否显示全部歌单分类
const isShowAllPlaylistCategory = ref<boolean>(false)
// 推荐歌曲
const recommendMusic = ref<IRecommendMusic>()
// 设置歌手背景图片
const getStyle = (item: any) => {
return {
"background-image": "url(" + item.picUrl + ")"
}
}
// 设置歌单分类样式
const getPlaylistTypeStyle = (index: number) => {
return {
"animation-delay": index * 25 + "ms"
}
}
//加载推荐歌手
const loadSingerList = async () => {
const { data } = await getHotSinger({ offset: 0, limit: 5 })
hotSingerData.value = data
}
// 加载歌单分类
const loadPlaylistCategory = async () => {
const { data } = await getPlaylistCategory()
playlistCategory.value = data
}
// 加载推荐歌曲
const loadRecommendMusic = async () => {
const { data } = await getRecommendMusic({ limit: 6 })
recommendMusic.value = data
}
// 页面初始化
onMounted(() => {
loadSingerList()
loadPlaylistCategory()
loadRecommendMusic()
})
</script>
<style lang="scss" scoped>
.recommend-singer {
&-list {
@apply flex mt-5;
height: 350px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
&-bg {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(80%);
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center;
}
}
}
}
.main-content {
@apply mt-6 flex;
.title {
@apply text-lg font-bold mb-4;
}
.play-list-type {
width: 250px;
@apply mr-6;
&-item,
&-showall {
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
background-color: #1a1a1a;
}
&-showall {
@apply block text-center;
}
}
.recommend-music {
}
}
</style>