网易云音乐移动端项目实战(分解上)

网易云音乐后台API服务搭建

步骤一
windows系统安装git

//傻瓜式安装,可以一直next

步骤二
网易云音乐 NodeJS 版 API

下载好后,在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中间的间距会比较大
*/

网易云音乐移动端项目实战(分解上)

三、导入轮播组件

vue3- swiper组件

 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

/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>

效果图
网易云音乐移动端项目实战(分解上)

上一篇:公司含硫原料生产


下一篇:keras网络模型