网易云音乐后台API服务搭建
//傻瓜式安装,可以一直next
下载好后,在vscode下新建一个文件夹
右击打开git bash here
输入
git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git
github地址
步骤三用
vscode打开下载好的文件夹
进入此文件夹后输入
cnpm install安装依赖
完毕输入
node app.js
运行完页面
步骤四
在终端中输入命令,创建一个新项目
vue create musicapp
在public文件夹下创建一个js文件,js文件创建一个rem布局文件
实现自适应REM布局
准备工作
一、实现自适应REM布局
rem.js
function remSize(){
//获取浏览器窗口文档显示区域的宽度,不包括滚动条。
var deviceWidth = document.documentElement.clientWidth || window.innerWidth
if(deviceWidth > 750){
deviceWidth = 750
}
if(deviceWidth <= 320){
deviceWidth = 320
}
//设计稿是750 设置一半的宽度那么设计稿的宽度1rem等于设计稿的100像素
document.documentElement.style.fontSize = (deviceWidth / 7.5) + 'px';
document.querySelector('body').style.fontSize = 0.3 + 'rem';
}
remSize()
window.onresize = function(){//onreset 事件在表单被重置后触发。
remSize()
}
index.html中导入rem.js
<script src="<%= BASE_URL %>js/rem.js"></script>
index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- <%= BASE_URL %>基础路径 -->
<script src="<%= BASE_URL %>js/rem.js"></script>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
字体图标
阿里巴巴矢量图图标库
加好购物车后添加至项目,可在线获取链接
引入到页面
二、头部导航布局与样式
效果如下
App.vue中添加topNav组件,设置全局字体图标样式
App.vue
<template>
<div id="nav">
<topNav/>
</div>
<!-- <router-view/> -->
</template>
<script>
import topNav from '../src/components/topNav.vue'
export default {
components: {
topNav
}
}
</script>
<style lang="less">
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
*{
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: "微软雅黑";
}
</style>
topNav.vue中设置topNav组件样式
topNav.vue
<template>
<div>
<div class="topNav">
<div class="topleft">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-caidan"></use>
</svg>
</div>
<div class="topmain">
<span class="navBtn">我的</span>
<span class="navBtn active">发现</span>
<span class="navBtn">云村</span>
<span class="navBtn">视频</span>
</div>
<div class="topright">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-sousuo"></use>
</svg>
</div>
</div>
</div>
</template>
<style socoped lang="less">
.topNav {
display: flex;
width: 7.5rem;
height: 1rem;
justify-content: space-between;
align-items: center;
padding:0 0.2rem;
//lang="less"
.icon{
width: 0.5rem;
height: 0.5rem;
}
.search{
width: 0.45rem;
height: 0.45rem;
}
}
.topmain{
width: 5rem;
display: flex;
justify-content: space-around;
.active{
font-weight: 900;
}
}
</style>
ps:
/*
space-between 最左、最右item贴合左侧或右侧边框,item与item之间间距相等。
space-around 每个item 左右方向的margin相等。两个item中间的间距会比较大
*/
三、导入轮播组件
npm install swiper@5.4.5
npm install vue-awesome-swiper@4.1.1
swiperzujian.vue
<template>
<div>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in imgs" :key="i"><img :src="item" alt=""></div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
</div>
</template>
<script>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
export default {
data:function(){
return{
imgs:[
require('../assets/imag/adpage1.jpg'),
require('../assets/imag/adpage2.jpg'),
require('../assets/imag/adpage3.jpg'),
]
}
},
components: {},
mounted() {
var mySwiper = new Swiper(".swiper-container", {
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination"
},
});
}
};
</script>
<style lang="less">
.swiper-container {
width: 7.1rem;
height: 3rem;
border-radius: 0.1rem;
}
.swiper-slide img{
width: 100%;
}
.swiper-pagination-bullet-active{
background-color: rgb(179, 178, 177);
}
</style>
四、封装请求获取网易的banner图
安装axios
npm install axios --save
/banner?type=2
启动搭建的服务器请求服务器地址加上接口地址
http://localhost:3000/banner?type=2
swiperzujian.vue
<template>
<div>
<div class="swiper-container" id="d1">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in imgs" :key="i">
<img :src="item.pic">
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
</div>
</template>
<script>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
import axios from "axios";
export default {
data: function() {
return {
imgs: [
//因为请求数据中banner是一个对象,img存放在pc属性上
{
pic: require("../assets/imag/adpage1.jpg")
},
{ pic: require("../assets/imag/adpage2.jpg") },
{ pic: require("../assets/imag/adpage3.jpg") },
{ pic: require("../assets/imag/adpage3.jpg") }
]
};
},
mounted() {
var mySwiper = new Swiper(".swiper-container", {
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable: true
}
});
async function ff() {
let res = await axios.get("http://localhost:3000/banner?type=2");
return res;
}
ff().then(res => {
this.imgs = res.data.banners;
});
}
};
</script>
<style lang="less">
#d1.swiper-container {
width: 7.1rem;
height: 2.8rem;
border-radius: 0.1rem;
}
.swiper-slide img {
width: 100%;
}
.swiper-pagination-bullet-active {
background-color: rgb(179, 178, 177);
}
</style>
封装一下
src下面的文件夹api下的index.js
import axios from 'axios';
//获取轮播图API
/*
0: pc
1: android
2: iphone
3: ipad
*/
export async function ff(type = 2) {
let res = await axios.get(`http://localhost:3000/banner?type=${type}`);
return res;
}
<template>
<div>
<div class="swiper-container" id="d1">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in imgs" :key="i">
<img :src="item.pic">
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
</div>
</template>
<script>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
import axios from "axios";
import {ff} from '../api/index.js'
export default {
data: function() {
return {
imgs: [
{
pic: require("../assets/imag/adpage1.jpg")
},
{ pic: require("../assets/imag/adpage2.jpg") },
{ pic: require("../assets/imag/adpage3.jpg") },
{ pic: require("../assets/imag/adpage3.jpg") }
]
};
},
mounted() {
var mySwiper = new Swiper(".swiper-container", {
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable: true
}
});
ff(2).then(res => {
this.imgs = res.data.banners;
});
}
};
</script>
<style lang="less">
#d1.swiper-container {
width: 7.1rem;
height: 2.8rem;
border-radius: 0.1rem;
}
.swiper-slide img {
width: 100%;
}
.swiper-pagination-bullet-active {
background-color: rgb(179, 178, 177);
}
</style>
网易云音乐效果图
五、图标列表组件
新建一个模板名为iconList.vue加入到App.vue中
iconList.vue代码(基础布局)
<template>
<div class="iconList">
<div class="iconItem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-tuijian"></use>
</svg>
<span>每日推荐</span>
</div>
<div class="iconItem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-vipsirenzhuanxiangdingzhiyewukehu"></use>
</svg>
<span>私人FM</span>
</div>
<div class="iconItem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-gedan"></use>
</svg>
<span>歌单</span>
</div>
<div class="iconItem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-paihangbang"></use>
</svg>
<span>排行榜</span>
</div>
</div>
</template>
<style lang="less" scoped>
.iconList{
display: flex;
padding: 0.34rem;
justify-content: space-between;
.icon{
height: 0.8rem;
width: 0.8rem;
}
.iconItem{
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
font-size: 0.28rem;
}
}
</style>
六、发现好歌单实现
新建模板musicList.vue并引入到App.vue
1.实现基本布局
2.设计好基本布局后做ajax请求
getMusicList(10).then(res => {
console.log(res);
this.musicList = res.data.result;
});
请求结果如下图
api中index.js
import axios from 'axios';
//获取轮播图API
/*
0: pc
1: android
2: iphone
3: ipad
*/
export async function ff(type = 2) {
return await axios.get(`http://localhost:3000/banner?type=${type}`);
}
//获取推荐歌单默认十条数据
export async function getMusicList(limit = 10){
return await axios.get(`http://localhost:3000/personalized?limit=${limit}`)
}
musicList.vue
<template>
<div class="musicList">
<div class="musicList-Top">
<div class="title">发现好歌单</div>
<div class="more">查看更多</div>
</div>
<div class="mlist">
<!-- swiper-container -->
<div class="swiper-container" id="d2">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in musicList" :key="i">
<img :src="item.picUrl">
<div class="name">{{item.name}}</div>
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{item.playCount}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script scoped>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
import { getMusicList } from "@/api/index.js";
export default {
data() {
return {
musicList: []
};
},
mounted() {
var swiper = new Swiper("#d2", {
slidesPerView: 3,
spaceBetween: 10
});
getMusicList(10).then(res => {
this.musicList = res.data.result;
});
},
updated() {
var swiper = new Swiper("#d2", {
slidesPerView: 3,
spaceBetween: 10
});
}
};
</script>
<style lang="less" scoped>
.musicList {
width: 7.5rem;
padding: 0.4rem;
.musicList-Top {
display: flex;
align-content: center;
justify-content: space-between;
height: 1rem;
.title {
font-size: 0.4rem;
font-weight: 900;
}
.more {
border: 1px solid #ccc;
border-radius: 0.1rem;
padding: 0.08rem;
font-size: 0.24rem;
text-align: center;
height: 0.5rem;
}
}
}
.mlist {
.swiper-container {
width: 100%;
height: 3rem;
.swiper-slide {
display: flex;
flex-direction: column;
position: relative;
img {
width: 100%;
height: auto;
border-radius: 0.1rem;
}
.name {
height: 0.6rem;
width: 100%;
font-size: 0.24rem;
line-height: 0.4rem;
text-align: center;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
}
}
}
}
}
</style>
过滤函数(尝试把播放量数组改成亿或者万,调用方法直接在变量中引用方法)
changeValue: function(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
<template>
<div class="musicList">
<div class="musicList-Top">
<div class="title">发现好歌单</div>
<div class="more">查看更多</div>
</div>
<div class="mlist">
<!-- swiper-container -->
<div class="swiper-container" id="d2">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in musicList" :key="i">
<img :src="item.picUrl">
<div class="name">{{item.name}}</div>
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{changeValue(item.playCount)}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script scoped>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
import { getMusicList } from "@/api/index.js";
export default {
data() {
return {
musicList: []
};
},
mounted() {
var swiper = new Swiper("#d2", {
slidesPerView: 3,
spaceBetween: 10
});
getMusicList(10).then(res => {
this.musicList = res.data.result;
});
},
updated() {
var swiper = new Swiper("#d2", {
slidesPerView: 3,
spaceBetween: 10
});
},
methods: {
//过滤函数
changeValue: function(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
}
};
</script>
<style lang="less" scoped>
.musicList {
width: 7.5rem;
padding: 0.4rem;
.musicList-Top {
display: flex;
align-content: center;
justify-content: space-between;
height: 1rem;
.title {
font-size: 0.4rem;
font-weight: 900;
}
.more {
border: 1px solid #ccc;
border-radius: 0.1rem;
padding: 0.08rem;
font-size: 0.24rem;
text-align: center;
height: 0.5rem;
}
}
}
.mlist {
.swiper-container {
width: 100%;
height: 3rem;
.swiper-slide {
display: flex;
flex-direction: column;
position: relative;
img {
width: 100%;
height: auto;
border-radius: 0.1rem;
}
.name {
height: 0.6rem;
width: 100%;
font-size: 0.24rem;
line-height: 0.4rem;
text-align: center;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
//svg用fill设置颜色
}
}
}
}
}
</style>
使用vue3实现好歌单功能
<template>
<div class="musicList">
<div class="musicList-Top">
<div class="title">发现好歌单</div>
<div class="more">查看更多</div>
</div>
<div class="mlist">
<!-- swiper-container -->
<div class="swiper-container" id="d2">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item,i) in musicList.musicLists" :key="i">
<img :src="item.picUrl">
<div class="name">{{item.name}}</div>
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{changeValue(item.playCount)}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script scoped>
import Swiper from "swiper";
import "swiper/css/swiper.css";
import "swiper/js/swiper.min.js";
import { getMusicList } from "@/api/index.js";
import { reactive, onMounted, onUpdated } from "vue";
export default {
setup() {
let musicList = reactive({ musicLists: [] });
function changeValue(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
onMounted(() => {
getMusicList(10).then(res => {
musicList.musicLists = res.data.result;
});
}),
onUpdated(() => {
var swiper = new Swiper("#d2", {
slidesPerView: 3,
spaceBetween: 10
});
});
return {
musicList,
changeValue
};
}
};
</script>
<style lang="less" scoped>
.musicList {
width: 7.5rem;
padding: 0.4rem;
.musicList-Top {
display: flex;
align-content: center;
justify-content: space-between;
height: 1rem;
.title {
font-size: 0.4rem;
font-weight: 900;
}
.more {
border: 1px solid #ccc;
border-radius: 0.1rem;
padding: 0.08rem;
font-size: 0.24rem;
text-align: center;
height: 0.5rem;
}
}
}
.mlist {
.swiper-container {
width: 100%;
height: 3rem;
.swiper-slide {
display: flex;
flex-direction: column;
position: relative;
img {
width: 100%;
height: auto;
border-radius: 0.1rem;
}
.name {
height: 0.6rem;
width: 100%;
font-size: 0.24rem;
line-height: 0.4rem;
text-align: center;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
//svg用fill设置颜色
}
}
}
}
}
</style>
七、setup中获取路由信息
通过路由获取id值
const router = useRouter()
let id = router.currentRoute._value.query.id
//获取歌单的详情
export async function getMusicContent(id){
return await axios.get(`${localhostUrl}/playlist/detail?id=${id}`)
}
api 下的index.js
import axios from 'axios';
//获取轮播图API
/*
0: pc
1: android
2: iphone
3: ipad
*/
let localhostUrl = 'http://localhost:3000'
export async function ff(type = 2) {
return await axios.get(`${localhostUrl}/banner?type=${type}`);
}
//获取推荐歌单默认十条数据
export async function getMusicList(limit = 10){
return await axios.get(`${localhostUrl}/personalized?limit=${limit}`)
}
//获取歌单的详情
export async function getMusicContent(id){
return await axios.get(`${localhostUrl}/playlist/detail?id=${id}`)
}
router 下的index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/listview',
name: 'listview',
component: () => import('../views/listview.vue')
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
views下的listview.vue
<template>
<div>
<h1>listview</h1>
</div>
</template>
<script>
import { getMusicContent } from "@/api/index.js";
import { reactive, onMounted, onUpdated } from "vue";
import { useRouter } from "vue-router";
//reactive响应式
export default {
setup() {
const router = useRouter();
let id = router.currentRoute._value.query.id;
let state = reactive({ list: [] });
onMounted(() => {
getMusicContent(id).then(res => {
});
});
}
};
</script>
<style lang="less" scoped>
</style>
views下的home
<template>
<div class="home">
<top-nav/>
<swiperzujian/>
<iconList/>
<musicList/>
</div>
</template>
<script>
import topNav from "@/components/topNav.vue";
import swiperzujian from "@/components/swiperzujian.vue";
import iconList from "@/components/iconList.vue";
import musicList from "@/components/musicList.vue";
export default {
name: "Home",
components: {
topNav,
swiperzujian,iconList,musicList
},
data() {
return {};
}
};
</script>
<style lang="less">
</style>
App.vue
<template>
<div id="nav">
<router-view/><!-- router-view标签是指路由,其实就是指向的意思 -->
</div>
</template>
<script>
export default {
components:{
}
}
</script>
<style lang="less">
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
*{
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: "微软雅黑";
}
a {
text-decoration: none;
color: #333;
}
</style>
<template>
<div class="listviewTop">
<img :src="playlist.coverImgUrl" class="bg">
<div class="listViewTopNav">
<div class="back">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-zuojiantou"></use>
</svg>
<div class="title">歌单</div>
</div>
<div class="right">
<svg class="icon search" aria-hidden="true">
<use xlink:href="#icon-sousuo1"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-diandiandianshu-copy"></use>
</svg>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["playlist"]
};
</script>
<style lang="less" scoped>
.listviewTop {
.bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: auto;
z-index: -1;
filter: blur(40px);
}
width: 7.5rem;
padding: 0 0.4rem;
height: 6rem;
font-size: 0.4rem;
.listViewTopNav {
line-height: 0.7rem;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: 1rem;
.back,
.right {
display: flex;
.icon {
font-size: 0.6rem;
color: #fff;
}
.search {
margin-right: 0.4rem;
}
}
.back {
.icon {
font-size: 0.7rem;
}
.title {
margin-left: 0.4rem;
}
}
}
}
</style>
<template>
<div class="listview" >
<listviewTop :playlist="state.playlist"/>
</div>
</template>
<script>
import { getMusicContent } from "@/api/index.js";
import { reactive, onMounted, onUpdated } from "vue";
import { useRouter ,useRoute} from "vue-router";
import listviewTop from "@/components/listviewTop.vue"
//reactive响应式
export default {
setup() {
const router = useRouter();
const route = useRoute();
//state是响应式对象,所以传它
let state = reactive({ list: [],playlist:{} });
onMounted(() => {
let id = router.currentRoute._value.query.id;
getMusicContent(id).then(res => {
console.log(res)
state.playlist = res.data.playlist;
});
});
return{
state
}
},
components:{
listviewTop
}
};
</script>
<style lang="less" scoped>
</style>
八、歌单详情内容
从数据中获取
listviewTop
<template>
<div class="listviewTop">
<img :src="playlist.coverImgUrl" class="bg">
<div class="listViewTopNav">
<div class="back">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-zuojiantou"></use>
</svg>
<div class="title">歌单</div>
</div>
<div class="right">
<svg class="icon search" aria-hidden="true">
<use xlink:href="#icon-sousuo1"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-diandiandianshu-copy"></use>
</svg>
</div>
</div>
<div class="content">
<div class="contentleft">
<img :src="playlist.coverImgUrl" class="imag">
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{changeValue(playlist.playCount)}}</span>
</div>
</div>
<div class="contentrigh">
<h4>{{playlist.name}}</h4>
<div class="author">
<div class="hearder">
<img :src="playlist.creator.avatarUrl">
<span>{{playlist.creator.nickname}}</span>
</div>
<div class="discription">{{playlist.description}}</div>
</div>
</div>
</div>
<div class="iconList">
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-IMliaotian-duihua"></use>
</svg>
<span>{{playlist.commentCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-fenxiang"></use>
</svg>
<span>{{playlist.shareCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xiazai"></use>
</svg>
<span>下载</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-checkbox"></use>
</svg>
<span>多选</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["playlist"],
setup() {
function changeValue(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
return {
changeValue
};
}
};
</script>
<style lang="less" scoped>
.listviewTop {
.bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: auto;
z-index: -1;
filter: blur(40px);
}
overflow: hidden;
width: 7.5rem;
padding: 0 0.4rem;
height: 6rem;
font-size: 0.4rem;
.listViewTopNav {
padding-top: 0.2rem;
line-height: 0.7rem;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: 1rem;
.back,
.right {
display: flex;
.icon {
font-size: 0.6rem;
color: #fff;
}
.search {
margin-right: 0.4rem;
}
}
.back {
.icon {
font-size: 0.7rem;
}
.title {
margin-left: 0.4rem;
}
}
}
.content {
padding-top: 0.5rem;
display: flex;
justify-content: space-between;
.contentleft {
position: relative;
img {
width: 3rem;
height: 3rem;
border-radius: 0.1rem;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
//svg用fill设置颜色
}
}
}
.contentrigh {
position: relative;
h4 {
color: #fff;
}
width: 3.3rem;
display: flex;
flex-direction: column;
.author {
flex: 1;
display: flex;
flex-direction: column;
align-content: center;
justify-content: flex-start;
height: 3rem;
.hearder {
padding-top: 0.1rem;
display: flex;
flex: 1;
span {
padding-left: 0.15rem;
font-size: 0.24rem;
color: rgb(240, 240, 240);
line-height: 0.7rem;
}
img {
width: 0.6rem;
height: 0.6rem;
border-radius: 0.3rem;
}
}
.discription {
position: absolute;
bottom: 0.1rem;
font-size: 0.24rem;
color: rgb(209, 209, 209);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
.iconList {
display: flex;
justify-content: space-between;
padding: 0 0.3rem;
padding-top: 0.3rem;
align-items: center;
.iconitem {
display: flex;
flex-direction: column;
color: #fff;
.icon {
font-size: 0.6rem;
}
span {
padding-top: 0.1rem;
font-size: 0.24rem;
line-height: 0.24rem;
text-align: center;
}
}
}
}
</style>
listview.vue
<template>
<div class="listview">
<listviewTop :playlist="state.playlist"/>
</div>
</template>
<script>
import { getMusicContent } from "@/api/index.js";
import { reactive, onMounted, onUpdated } from "vue";
import { useRouter, useRoute } from "vue-router";
import listviewTop from "@/components/listviewTop.vue";
//reactive响应式
export default {
setup() {
const router = useRouter();
const route = useRoute();
//state是响应式对象,所以传它
let state = reactive({ list: [], playlist: { creator: {} } });
onMounted(() => {
let id = router.currentRoute._value.query.id;
getMusicContent(id).then(res => {
state.playlist = res.data.playlist;
console.log(res);
});
});
return {
state
};
},
components: {
listviewTop
}
};
</script>
<style lang="less" scoped>
</style>
详情页的图标列表与返回页面
<template>
<div class="listviewTop">
<img :src="playlist.coverImgUrl" class="bg">
<div class="listViewTopNav">
<div class="back" @click="$router.go(-1)">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-zuojiantou"></use>
</svg>
<div class="title">歌单</div>
</div>
<div class="right">
<svg class="icon search" aria-hidden="true">
<use xlink:href="#icon-sousuo1"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-diandiandianshu-copy"></use>
</svg>
</div>
</div>
<div class="content">
<div class="contentleft">
<img :src="playlist.coverImgUrl" class="imag">
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{changeValue(playlist.playCount)}}</span>
</div>
</div>
<div class="contentrigh">
<h4>{{playlist.name}}</h4>
<div class="author">
<div class="hearder">
<img :src="playlist.creator.avatarUrl">
<span>{{playlist.creator.nickname}}</span>
</div>
<div class="discription">{{playlist.description}}</div>
</div>
</div>
</div>
<div class="iconList">
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-IMliaotian-duihua"></use>
</svg>
<span>{{playlist.commentCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-fenxiang"></use>
</svg>
<span>{{playlist.shareCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xiazai"></use>
</svg>
<span>下载</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-checkbox"></use>
</svg>
<span>多选</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["playlist"],
setup() {
function changeValue(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
return {
changeValue
};
}
};
</script>
<style lang="less" scoped>
.listviewTop {
.bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: auto;
z-index: -1;
filter: blur(40px);
}
overflow: hidden;
width: 7.5rem;
padding: 0 0.4rem;
height: 6rem;
font-size: 0.4rem;
.listViewTopNav {
padding-top: 0.2rem;
line-height: 0.7rem;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: 1rem;
.back,
.right {
display: flex;
.icon {
font-size: 0.6rem;
color: #fff;
}
.search {
margin-right: 0.4rem;
}
}
.back {
.icon {
font-size: 0.7rem;
}
.title {
margin-left: 0.4rem;
}
}
}
.content {
padding-top: 0.5rem;
display: flex;
justify-content: space-between;
.contentleft {
position: relative;
img {
width: 3rem;
height: 3rem;
border-radius: 0.1rem;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
//svg用fill设置颜色
}
}
}
.contentrigh {
position: relative;
h4 {
color: #fff;
}
width: 3.3rem;
display: flex;
flex-direction: column;
.author {
flex: 1;
display: flex;
flex-direction: column;
align-content: center;
justify-content: flex-start;
height: 3rem;
.hearder {
padding-top: 0.1rem;
display: flex;
flex: 1;
span {
padding-left: 0.15rem;
font-size: 0.24rem;
color: rgb(240, 240, 240);
line-height: 0.7rem;
}
img {
width: 0.6rem;
height: 0.6rem;
border-radius: 0.3rem;
}
}
.discription {
position: absolute;
bottom: 0.1rem;
font-size: 0.24rem;
color: rgb(209, 209, 209);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
.iconList {
display: flex;
justify-content: space-between;
padding: 0 0.3rem;
padding-top: 0.3rem;
align-items: center;
.iconitem {
display: flex;
flex-direction: column;
color: #fff;
.icon {
font-size: 0.6rem;
}
span {
padding-top: 0.1rem;
font-size: 0.24rem;
line-height: 0.24rem;
text-align: center;
}
}
}
}
</style>
九、播放列表实现
还是基本布局以及循环拿到的数据
playlist.vue
<template>
<div class="playlist">
<div class="playlist-Top">
<div class="left">
<svg class="icon search" aria-hidden="true">
<use xlink:href="#icon-bofang"></use>
</svg>
<div class="com1">
<div class="com2">
<div class="title">播放全部</div>
<div class="num">(共{{playlist.tracks.length}}首)</div>
</div>
</div>
</div>
<div class="btn">+收藏({{playlist.subscribedCount}})</div>
</div>
<div class="list">
<div class="listitem" v-for="(item,i) in playlist.tracks" :key="i">
<div class="playCount">{{i+1}}</div>
<div class="playcontent">
<div class="h4">{{item.name}}</div>
<div class="author">
<span class="tag" v-for="(item,i) in playlist.tags" :key="i">{{item}}</span>
<div class="discription">{{item.al.name}}</div>
</div>
</div>
<div class="playicon">
<svg class="icon play" aria-hidden="true">
<use xlink:href="#icon-bofang2"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-diandian"></use>
</svg>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["playlist"]
};
</script>
<style lang="less" scoped>
.playlist {
border-top-left-radius: 0.3rem;
border-top-right-radius: 0.3rem;
background-color: #fff;
width: 7.5rem;
.playlist-Top {
position: relative;
display: flex;
height: 1.2rem;
align-items: center;
width: 7.5rem;
justify-content: space-between;
.left {
width: 6.7rem;
flex: 1;
display: flex;
font-size: 0.4rem;
padding-left: 0.2rem;
.icon {
width: 0.5rem;
height: 0.5rem;
font-size: 0.5rem;
}
.com1 {
width: 5.5rem;
margin-left: 0.3rem;
display: flex;
font-size: 0.34rem;
font-family: "微软雅黑";
color: #333;
.com2 {
display: flex;
align-items: center;
.num {
line-height: 0.3rem;
font-size: 0.3rem;
color: rgba(187, 185, 185, 0.664);
}
}
}
}
.btn {
position: absolute;
right: 0.15rem;
font-size: 0.27rem;
color: #fff;
height: 0.85rem;
line-height: 0.85rem;
text-align: center;
border-radius: 0.4rem;
width: 2.4rem;
background-color: #ff4935;
}
}
.list {
position: relative;
width: 7.5rem;
height: 1.2rem;
.listitem {
.playCount {
height: 1.2rem;
width: 1rem;
text-align: center;
line-height: 1.2rem;
color: rgb(165, 164, 164);
font-size: 0.36rem;
}
background-color: #fff;
display: flex;
position: relative;
.playcontent {
.h4 {
padding-top: 0.1rem;
display: flex;
align-items: center;
height: 0.85rem;
font-size: 0.3rem;
}
.author {
bottom: 0.1rem;
position: absolute;
height: 0.35rem;
display: flex;
align-items: center;
span {
width: 2.8em;
text-align: center;
height: 0.25rem;
color: rgb(250, 43, 43);
border-radius: 3px;
font-size: 0.16rem;
line-height: 0.2rem;
border: 0.5px solid #ee8888;
background-color: #ffd0c5a4;
margin-right: 0.1rem;
}
.discription {
color: #c2bdbd;
height: 0.3rem;
line-height: 0.3rem;
font-size: 0.25rem;
}
}
}
.playicon {
// z-index: -1;
// position: fixed;
position: absolute;
right: 0.25rem;
height: 1.2rem;
line-height: 1.2rem;
text-align: center;
margin-top: 0.1rem;
.icon {
font-size: 0.5rem;
}
.play{
margin-right: 0.2rem;
}
}
}
}
}
</style>
public文件夹下的js文件夹的index.js
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="//at.alicdn.com/t/font_3042525_nvfyrtl9iw.js"></script>
</head>
<body>
<script src="<%= BASE_URL %>js/rem.js"></script>
</noscript>
<div id="app"></div>
</body>
</html>
listview.vue
<template>
<div class="listview">
<listviewTop :playlist="state.playlist"/>
<playlist :playlist="state.playlist"/>
</div>
</template>
<script>
import { getMusicContent } from "@/api/index.js";
import { reactive, onMounted, onUpdated } from "vue";
import { useRouter, useRoute } from "vue-router";
import listviewTop from "@/components/listviewTop.vue";
import playlist from "@/components/playlist.vue"
//reactive响应式
export default {
setup() {
const router = useRouter();
const route = useRoute();
//state是响应式对象,所以传它
let state = reactive({ list: [], playlist: { creator: {},tracks:{} } });
onMounted(() => {
let id = router.currentRoute._value.query.id;
getMusicContent(id).then(res => {
state.playlist = res.data.playlist;
console.log(res);
});
});
return {
state
};
},
components: {
listviewTop,playlist
}
};
</script>
<style lang="less" scoped>
.listview{
display: flex;
flex-direction: column;
}
</style>
listviewTop.vue
<template>
<div class="listviewTop">
<img :src="playlist.coverImgUrl" class="bg">
<div class="listViewTopNav">
<div class="back" @click="$router.go(-1)">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-zuojiantou"></use>
</svg>
<div class="title">歌单</div>
</div>
<div class="right">
<svg class="icon search" aria-hidden="true">
<use xlink:href="#icon-sousuo1"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-diandiandianshu-copy"></use>
</svg>
</div>
</div>
<div class="content">
<div class="contentleft">
<img :src="playlist.coverImgUrl" class="imag">
<div class="count">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-whiteplayCircle"></use>
</svg>
<span>{{changeValue(playlist.playCount)}}</span>
</div>
</div>
<div class="contentrigh">
<h4>{{playlist.name}}</h4>
<div class="author">
<div class="hearder">
<img :src="playlist.creator.avatarUrl">
<span>{{playlist.creator.nickname}}</span>
</div>
<div class="discription">{{playlist.description}}</div>
</div>
</div>
</div>
<div class="iconList">
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-IMliaotian-duihua"></use>
</svg>
<span>{{playlist.commentCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-fenxiang"></use>
</svg>
<span>{{playlist.shareCount}}</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xiazai"></use>
</svg>
<span>下载</span>
</div>
<div class="iconitem">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-checkbox"></use>
</svg>
<span>多选</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["playlist"],
setup() {
function changeValue(num) {
let res = 0;
if (num > 100000000) {
res = num / 100000000;
res = res.toFixed(2) + "亿";
} else if (num > 10000) {
res = num / 10000;
res = res.toFixed(2) + "万";
}
return res;
}
return {
changeValue
};
}
};
</script>
<style lang="less" scoped>
.listviewTop {
.bg {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: auto;
z-index: -1;
filter: blur(40px);
}
overflow: hidden;
width: 7.5rem;
padding: 0 0.4rem;
font-size: 0.4rem;
.listViewTopNav {
padding-top: 0.2rem;
line-height: 0.7rem;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: 1rem;
.back,
.right {
display: flex;
.icon {
font-size: 0.6rem;
color: #fff;
}
.search {
margin-right: 0.4rem;
}
}
.back {
.icon {
font-size: 0.7rem;
}
.title {
margin-left: 0.4rem;
}
}
}
.content {
padding-top: 0.5rem;
display: flex;
justify-content: space-between;
.contentleft {
position: relative;
img {
width: 3rem;
height: 3rem;
border-radius: 0.1rem;
}
.count {
position: absolute;
right: 0.1rem;
top: 0.1rem;
color: rgb(253, 251, 251);
font-size: 0.2rem;
display: flex;
align-items: center;
.icon {
font-size: 0.2rem;
//svg用fill设置颜色
}
}
}
.contentrigh {
position: relative;
h4 {
color: #fff;
}
width: 3.3rem;
display: flex;
flex-direction: column;
.author {
flex: 1;
display: flex;
flex-direction: column;
align-content: center;
justify-content: flex-start;
height: 3rem;
.hearder {
padding-top: 0.1rem;
display: flex;
flex: 1;
span {
padding-left: 0.15rem;
font-size: 0.24rem;
color: rgb(240, 240, 240);
line-height: 0.7rem;
}
img {
width: 0.6rem;
height: 0.6rem;
border-radius: 0.3rem;
}
}
.discription {
position: absolute;
bottom: 0.1rem;
font-size: 0.24rem;
color: rgb(209, 209, 209);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
.iconList {
height: 1.5rem;
display: flex;
justify-content: space-between;
padding: 0 0.3rem;
padding-top: 0.3rem;
align-items: center;
.iconitem {
display: flex;
flex-direction: column;
color: #fff;
.icon {
font-size: 0.6rem;
}
span {
padding-top: 0.1rem;
font-size: 0.24rem;
line-height: 0.24rem;
text-align: center;
}
}
}
}
</style>
效果图