vue2+vant--网易云项目实现

  • 小妹随手有感而写,各位大佬嘴下留情!!!

  1. 下载网易云音乐node接口项目, 在本地启动, 为我们vue项目提供数据支持(需预先下载nodejs,这里就不详细说明了)

项目地址(https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%e5%ae%89%e8%a3%85)

下载并安装所有依赖后,打开项目终端,输入

node app.js

运行后,看到如下页面就成功了

  1. vant安装

  • 右键项目名称,点击'使用命令行打开所在目录',通过npm安装

npm i @vant/weapp -S --production

详细教程参考(快速上手 - Vant Weapp (vant-ui.github.io)

  1. router index.js路由配置

部分页面使用了路由懒加载功能,Login页面使用了路由拦截

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store/index.js'
import Login from '@/views/Login.vue'
import Look from '@/views/Look.vue'
import Cloud from '@/views/Cloud.vue'
import ShiPing from '@/views/ShiPing.vue'
import FooterMusic from '@/views/FooterMusic.vue'
import Search from '@/views/Home/Search.vue'
import InforUser from '@/views/InforUser.vue'
import Commoent from '@/views/Home/Commoent.vue'
import Detailboder from '@/views/Home/DetailBoder.vue'
Vue.use(VueRouter)

const routes = [{
        path: "/login",
        component: Login,
        meta: {
            keepalive: false
        }
    },
    {
        path: "/look",
        component: Look,
        children: [

        ],
        meta: {
            keepAlive:false
        },
    },

    {
        path: "/cloud",
        component: Cloud,
        meta: {
            keepalive: false
        }
    },
    {
        path: "/shiping",
        component: ShiPing,
        meta: {
            keepalive: false
        }
    },
    {
        path: '/detail',
        // 路由懒加载
        component: () => import('@/views/Home/DetailMusic.vue'),
        name: 'detail',
        meta: {
            keepAlive: true
        },
    },
    {
        path: '/lyrics',
        component: () => import('@/views/LyRics.vue'),
        name: 'lyrics',
    },
    {
        path: '/footermusic',
        component: FooterMusic,
        meta: {
            keepalive: false
        }
    },
    {
        path: '/search',
        component: Search,
        meta: {
            keepalive: false
        }
    },
    {
        path: '/inforuser',
        component:InforUser,
        meta: {
            keepalive: false
        },
        // 路由拦截
        beforeEnter:(to,from,next)=>{
            if(store.state.islogin){
                next()
            }else{
                next('/login')
                store.state.isfootermusic=false
            }
        }
    },
    {
        path:'/commoent',
        component:Commoent,
        name:'commoent',
        meta: {
            keepalive: true
        },
    },
    {
        path:'/leaderboard',
        component: () => import('@/views/Leaderboard.vue'),
        meta: {
            keepalive: false
        },
    },
    {
        path:'/detailboder',
        component:Detailboder,
        name:'detailboder'
    },
    {
        path: '*',
        redirect: '/look'
    }
]

const router = new VueRouter({
    mode: 'hash',
    routes
})
export default router
  1. store index.js配置

state为公共状态

mutations存储方法

getters,actions, modules暂时没有用到

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    // state公共状态
  state: {
      list: [{
          al: {
              id: 145750496,
              name: "镜象马戏团",
              pic: 109951167493773280,
              picUrl: "https://p1.music.126.net/jFkiKK1EtAmkOPeCjBK1Wg==/109951167493773275.jpg",
              pic_str: "109951167493773275",
          },
          id: 1951980693,
        name:"ONE"
      }],
      // 默认下标为0
      playindex: 0,
      // 歌曲详情页
      detailshow:false,
      //歌词
      lyriclist:{},
      //播放的当前时间
      currentime:0,
      //歌曲总时长
      duration:0,
      // 是否登录
      islogin:false,
      //判断底部组件是否显示
      isfootermusic:true,
  },
  getters: {
  },
  mutations: {
      upchangelogin:function(state){
          state.islogin=true
      },
      upchangefooter:function(state){
          state.isfootermusic=true
      },
      updatePlayList:function(state,value){
          state.list=value
          // console.log(state.list)
      },
      updateIndex:function(state,value){
          state.playindex=value
      },
      UpdetailShow:function(state){
          state.detailshow=!state.detailshow
      },
      uplyricslist:function(state,value){
          state.lyriclist=value
          // console.log(state.lyriclist)
      },
      upcurrentime:function(state,value){
          state.currentime=value
      },
      upduration:function(state,value){
          state.duration=value
          // console.log(state.duration)
      },
      pushlist:function(state,value){
          state.list.push(value)
          // console.log(value)
      },
  },
  actions: {
      
  },
  modules: {
  }
})
  1. 各个组件详细代码

  • Look.vue 发现页面,也是首页面,如图

<template>
    <div>
        <tabar></tabar>
        <swiper></swiper>
        <icon></icon>
        <music></music>
        <router-view></router-view>
    </div>
</template>

<script>
    import tabar from '@/views/TaBar'
    import swiper from '@/views/Home/SwiperTop.vue'
    import icon from '@/views/Home/IconList'
    import music from '@/views/Home/MusicList'
    import Vue from 'vue'
    import Vant from 'vant'
    import 'vant/lib/index.css'
    Vue.use(Vant)
    export default{
        components:{
            tabar,
            swiper,
            icon,
            music,
        },
        // activated是页面激活后的钩子函数,一进页面就会触发
         activated () {
          // 显示时
            console.log(1)
          },
          // deactivated 离开页面的钩子函数,一离开页面就会触发
          deactivated () {
            // 不显示时
            console.log(2)
          },

    }
</script>

<style>
</style>

Look页面包含的子组件

  • TaBar.vue

<template>
    <div class="topnav">
        <div class="topleft">
        <van-icon name="apps-o" size="1.5rem" />
        </div>
    <ul class="top-content">
              <!-- 声明式导航 -->
             <li><router-link to="/inforuser" active-class="hhh">我的</router-link></li>         
            <li><router-link to="/look" active-class="hhh" >发现</router-link></li>
             <li><router-link to="/cloud" active-class="hhh">云村</router-link></li>
              <li><router-link to="/shiping" active-class="hhh">视频</router-link></li>
    </ul>
    <div class="topright">
    <van-icon name="search" size="1.5rem" @click="$router.push('/search')"/>
    </div>
    </div>
</template>

<script>
</script>

<style>
.hhh{
    color:red;
    font-weight: 1000;
    font-size: 1.25rem;
}
.topnav{
        width:100%;
        height:2.5rem;
        display:flex;
        justify-content: space-between;
        line-height: 2.5rem;
    }
    .top-content{
        width:65%;
        display:flex;
        height: 100%;
        justify-content: space-around;
        cursor: pointer;
    }
    .topleft{
        padding-left: 0.625rem;
    }
    .topright{
        padding-right: 0.625rem;
    }
</style>
  • SwiperTop.vue 放在views下面新建一个Home文件夹里

<template>
    <div id="swiperTop">
    <van-swipe :autoplay="3000" indicator-color="red">
        
      <van-swipe-item v-for="(item, index) in images" :key="index" >
          <span class="left"><van-icon name="arrow-left" size="20px"/></span>
        <img :src="item.pic"/>
        <span class="right"><van-icon name="arrow" size="20px"/></span>
      </van-swipe-item>
      
    </van-swipe>
    </div>
</template>

<script>
    import Vue from 'vue'
    import { Lazyload } from 'vant';
    import axios from 'axios'
    
    Vue.use(Lazyload);
    
    export default {
      data() {
        return {
          images: [],
        };
      },
      created(){
          axios.get(`http://localhost:3000/dj/banner`).then((item)=>{
              // console.log(item.data.data)
              this.images=item.data.data
          })
      }
    };
</script>

<style scoped="">
#swiperTop .van-swipe{
     width:100%;
     height:18.75rem;
 }
 .van-swipe-item img{
     width:100%;
     height:18.75rem;
     border-radius: 2rem;
 }
 .left{
     float:left;
     position: absolute;
     left:0px;
     top:9.375rem;
 }
 .right{
     float:right;
     position: absolute;
     right:0px;
     top:9.375rem
 }
</style>
  • IconList.vue 放在Home文件夹下

<template>
    <div class="iconlist">
        <div class="iconitem">
            <van-icon name="service" size="30px" color="red"/>
            <span>每日推荐</span>
        </div>
        <div class="iconitem">
            <van-icon name="like" size="30px"/>
            <span>私人FM</span>
        </div>
        <div class="iconitem" @click="$router.push('/leaderboard')">
            <van-icon name="fire" size="30px" color="red"/>
            <span>排行榜</span>
        </div>
        <div class="iconitem">
            <van-icon name="music" size="30px"/>
            <span>歌单</span>
        </div>
    </div>
</template>

<script>
</script>

<style>
    .iconlist{
        width:100%;
        height:2.5rem;
        line-height: 2.5rem;
        display:flex;
        justify-content: space-around;
        align-items: center;
    }
    .iconitem{
        width:25%;
        height:100%;
        display:flex;
        flex-direction: column;
        align-items: center;
        padding-top: 0.5rem;
    }
</style>
  • MusicList.vue 放在Home文件夹下

<template>
    <div class="music">
        <div class="musictop">
            <div class="title">发现好歌单</div>
            <div class="more">查看更多</div>
        </div>
        <div class="music-content">
            <van-swipe :width="150" class="my-swiper" 
            :show-indicators="false">
              <van-swipe-item v-for="(item,index) in list" :key="index" @click="detail(item)">
                  <img :src="item.coverImgUrl" alt="" class="img">
                  <span class="playcount">
                      <van-icon name="play-circle-o" size="20px"/>
                      {{item.playCount|playcount-filter}}
                  </span>
                  <span class="name">
                     {{item.name}}
                  </span>
                  </van-swipe-item>
            </van-swipe>
        </div>
    </div>
</template>

<script>
    import axios from 'axios'
    import Vue from 'vue'
    Vue.filter('playcount-filter',function(data){
        if(data>=100000000){
            return (data/100000000).toFixed(1)+"亿"
        }else if(data>=10000){
            return (data/10000).toFixed(2)+"万"
        }
    })
    export default{
        data(){
            return {
                list:[]
            }
        },
        created(){
            //歌单网友精选碟
            axios.get('http://localhost:3000/top/playlist/highquality').then((item)=>{
                // console.log(item.data.playlists)
                this.list=item.data.playlists
            })
        },
        methods:{
            detail (item) {
                // console.log(item)
              this.$router.push({ name: 'detail', params: { id: item.id } })
              // this.$router.push({ name: 'commoent', params: { id: item.id } })
            }
        }
    }
</script>

<style>
    .music{
        width:100%;
        height:2.5rem;
        padding:1.5rem;
        margin-top: 1.25rem;
    }
    .musictop{
        width:100%;
        height:1rem;
        display:flex;
        justify-content: space-between;
        margin-bottom:0.3125rem;
    }
    .title{
        font-size: 1.0625rem;
        font-weight: 800;
    }
    .more{
        border:1px solid #000000;
        text-align: center;
        line-height: 1.5rem;
        padding: 0 0.5rem;
        border-radius: 0.9375rem;
        height:1.5rem
    }
    .music-content{
        width:100%;
        height: 12.5rem;
    }
    .my-swiper{
        height:100%;
    }
    .img{
        width:100%;
        height:8.5rem;
        padding-right: 0.5rem;
        padding-top: 8px;
        border-radius: 0.8rem;
    }
    .name{
         overflow: hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-line-clamp: 1;
            -webkit-box-orient: vertical;
    }
</style>
  • Search.vue 搜索功能,放在Home文件夹下,效果如图

注(返回图标我使用的是阿里SVG代码,比较长,可以修改为自己喜欢的图标)

<template>
    <div>
    <div class="searchTop">
        <svg t="1675048038409" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2501"
         width="30" height="30" @click="$router.push('/look')">
            <path d="M929.70745 487.72513 167.942967 487.72513l358.793666-318.918493c12.390191-11.012821 13.505595-29.982872 2.493797-42.37204-11.010775-12.388145-29.979802-13.506619-42.369993-2.492774L74.839499 490.168786c-6.407943 5.695722-10.073426 13.859659-10.073426 22.432918 0 8.573259 3.665483 16.737196 10.073426 22.432918l412.019914 366.227985c5.717212 5.082762 12.83533 7.581676 19.926842 7.581676 8.275477-0.002047 16.515139-3.403516 22.443152-10.07445 11.012821-12.389168 9.897418-31.359218-2.493797-42.37204L179.893136 548.100196l749.814314 0c16.575514 0 30.013571-13.612019 30.013571-30.187533S946.283987 487.72513 929.70745 487.72513z"
             fill="#272636" p-id="2502"></path>
        </svg>
        <input type="text" placeholder="王靖雯不胖" v-model="searchkey" @keydown.enter="enterkey">
    </div>
    <div class="search-history">
        <span class="search-span">历史</span>
        <span v-for="(item,index) in keyword" :key="index" class="history" @click="searchhistory(item)">
            {{item}}
        </span>
        <van-icon name="delete-o" size="30px" style="position: absolute;right:0.625rem" @click="delhistory"/>
    </div>
    
    <div class="musiclist">
        <div v-for="(item,index) in searchresult" :key="item.id">
            <div class="music-detail" @click="dateIndex(item)">
                <div class="detail-left">
                    <span style="font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
                    <span class="singer">
                        <span class="tomusic" v-if="item.al.name?item.al.name:item.al.name='佚歌'">{{item.al.name}}</span>
                        <div class="tomusic">
                        <span class="writer-music" style="" v-for="res in item.ar">{{res.name}}&nbsp;&nbsp;</span>    
                        </div>
                    </span>
                </div>
                <van-icon name="play-circle-o" size="30px" v-if="item.mv!=0"/>
                <van-icon name="bars" size="30px"/>
            </div>
        </div>
    </div>
    </div>
</div>
</template>

<script>
    import axios from 'axios'
    import {mapState,mapMutations,mapAction } from 'vuex'
    export default{
        data(){
            return{
                keyword:[],
                
                searchkey:"",
                // 搜索结果
                searchresult:[]
            }
        },
        computed:{
            ...mapState(["list"])
        },
        created(){
            
        },
        mounted(){
            // 判读本地数据是否存在
            if(JSON.parse(localStorage.getItem('keyword')))
            {
            this.keyword=JSON.parse(localStorage.getItem('keyword'))    
            }
            else{
                this.keyword=[]
            }
        },
        methods:{
            // 回车添加
            enterkey(){
                if(this.searchkey!="")
                {
                    // unshift添加到数组首位
                    this.keyword.unshift(this.searchkey)
                    //去重
                    this.keyword=[...new Set(this.keyword)]
                    // 固定长度
                    if(this.keyword.length>8)
                    {
                        this.keyword.splice(this.keyword.length-1,1)
                    }
                    localStorage.setItem("keyword",JSON.stringify(this.keyword))
                    // 搜索结果
                    axios.get(`http://localhost:3000/cloudsearch?keywords=${this.searchkey}`)
                    .then(item=>{
                        this.searchresult=item.data.result.songs
                    })
                    this.searchkey=""
                }
            },
            // 删除按钮
            delhistory(){
                localStorage.removeItem("keyword")
                this.keyword=[]
            },
            searchhistory(item){
                axios.get(`http://localhost:3000/cloudsearch?keywords=${item}`)
                .then(res=>{
                    console.log(res.data.result.songs)
                    this.searchresult=res.data.result.songs
                })
            },
            dateIndex(item){
                this.pushlist(item)
                this.updateIndex(this.list.length-1)
            },
            ...mapMutations(["pushlist","updateIndex"])
        },
    }
</script>

<style>
    .writer-music {
        font-size: 10px;
        color: #65737E;
    }
    
    .tomusic {
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
        font-size: 1.3rem;
        width: 10rem;
    }
    
    .detail-left {
        width: 60%;
        display: flex;
        justify-content: space-around;
    }
    
    .singer {
        display: flex;
        flex-direction: column;
        padding-top: 0.625rem;
        padding-left: 0.625rem;
    }
    
    .music-detail {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .musiclist {
        width: 100%;
        background-color: #fff;
        padding-bottom: 4.25rem;
    }
    .aaa{
        border-top-left-radius: 1rem;
        border-top-right-radius: 1rem;
        border:1px solid #000000;
    }
    .search-history{
        width:100%;
        padding: 0.625rem;
        position: relative;
    }
    .search-span{
        font-size: 1.25rem;
        font-weight: 800;
    }
    .history{
        background-color: #a5a5a54d;
        padding: 0.5rem;
        border-radius: 0.6rem;
        margin:0.3125rem 0.625rem;
        display: inline-block;
    }
    .searchTop{
        width:100%;
        height:3rem;
        padding: 0.625rem;
        display:flex;
        align-items: center;
    }
    .searchTop input{
        margin-left: 1rem;
        border:none;
        border-bottom: 1px solid #000000;
        width:90%;
        padding:0.4rem;
    }
</style>
  • Leaderboard.vue 排行榜,放在view 文件夹下,效果如图

<template>
    <div>
        <!-- 头部 -->
        <van-sticky>
            <div class="header flex">
                <div class="header-left flex">
                    <van-icon name="arrow-left" size="30px" color="black" @click="$router.go(-1)" />
                    <span style="letter-spacing: 0.2rem;">网易排行榜</span>
                </div>
                <div class="header-right">
                    <van-icon name="chat-o" size="40" />
                </div>
            </div>
        </van-sticky>

        <!-- 各个榜单 -->
        <van-tabs swipeable animated>
            <van-tab title="官方" title-style="font-size:1.3rem">
                <div class="title">
                    榜单推荐
                </div>
                <div class="classify flex">
                    <span v-for="(item,index) in list.slice(0,3)" :key="index" class="classify-span" @click="Detailboder(item)">
                        <img :src="item.coverImgUrl" alt="" class="classify-img">
                        <span class="playcount">
                            <van-icon name="play-circle-o" size="30px" color="white" />
                            {{item.playCount|playcount-filter}}
                        </span>
                    </span>
                </div>
                <div class="title">
                    官方榜
                </div>
                <div style="padding-bottom: 5rem;">
                    <div v-for="(item,i) in list.slice(3,7)" :key="i" class="title-span" @click="Detailboder(item)">
                        <div class="title-left">
                            <span style="font-size: 25px;padding-bottom: 0.5rem;padding-left: 0.625rem;">{{item.name}}</span>
                            <div style="padding-top: 0.9375rem;">
                                <img :src="img[i]" alt="" class="title-img">
                            </div>
                        </div>
                        <div class="title-right">
                            <span class="state">{{item.updateFrequency}}</span>
                            <div class="title_cloum" v-for="(item,index) in music.slice(0,3)" :key="index">
                                <span style="padding-right: 0.625rem;">{{index+1}}</span>
                                <span>{{item.name}}</span>
                            </div>
                        </div>
                    </div>
                </div>
            </van-tab>
            <van-tab title="精选" title-style="font-size:1.3rem">
                <div v-for="(item,index) in list.slice(7,16)" :key="index" class="classify-span" @click="Detailboder(item)">
                    <img :src="item.coverImgUrl" alt="" class="classify_img">
                    <span class="play-count">
                        <van-icon name="play-circle-o" size="30px" color="white" />
                        {{item.playCount|playcount-filter}}
                    </span>
                </div>
            </van-tab>
            <van-tab title="曲风" title-style="font-size:1.3rem">
                <div v-for="(item,index) in list.slice(16,25)" :key="index" class="classify-span" @click="Detailboder(item)">
                    <img :src="item.coverImgUrl" alt="" class="classify_img">
                    <span class="play-count">
                        <van-icon name="play-circle-o" size="30px" color="white" />
                        {{item.playCount|playcount-filter}}
                    </span>
                </div>
            </van-tab>
            <van-tab title="全球" title-style="font-size:1.3rem">
                <div v-for="(item,index) in list.slice(25,32)" :key="index" class="classify-span" @click="Detailboder(item)">
                    <img :src="item.coverImgUrl" alt="" class="classify_img">
                    <span class="play-count">
                        <van-icon name="play-circle-o" size="30px" color="white" />
                        {{item.playCount|playcount-filter}}
                    </span>
                </div>
            </van-tab>
        </van-tabs>

    </div>
</template>

<script>
    import axios from 'axios'
    import Vue from 'vue'
    Vue.filter('playcount-filter', function(data) {
        if (data >= 100000000) {
            return (data / 100000000).toFixed(1) + "亿"
        } else if (data >= 10000) {
            return (data / 10000).toFixed(2) + "万"
        }
    })
    export default {
        data() {
            return {
                list: [],
                img: ["http://p4.music.126.net/IuyhHbgZVnFHLjknh3JriQ==/109951168223326749.jpg",
                    "http://p3.music.126.net/o_0_oXHXHUtkP-Y7x1JNHw==/109951168182087340.jpg",
                    "http://p4.music.126.net/74vJWfltitQodfT1VHImAQ==/109951168274510240.jpg",
                    "http://p4.music.126.net/as0q_BRqOMImCd0OsyU05Q==/109951168145373390.jpg"
                ],
                music: []
            }
        },
        created() {
            axios.get(`http://localhost:3000/toplist`).then(res => {
                console.log(res)
                this.list = res.data.list
                for (var i = 0; i < 4; i++) {
                    axios.get(`http://localhost:3000/playlist/detail?id=${this.list[i].id}`).then(item => {
                        // console.log(item)
                        for (var j = 0; j < 3; j++) {
                            this.music.push(item.data.playlist.tracks[j])
                        }
                    })
                }
                // console.log(this.list)
                console.log(this.music)
            })

        },
        methods: {
            Detailboder(item) {
                this.$router.push({
                    name: 'detailboder',
                    params: {
                        id: item.id,
                        img: item.coverImgUrl,
                        playcount: item.playCount,
                        subscribedCount: item.subscribedCount,
                        trackCount: item.trackCount
                    }
                })
                // this.$route.push('/detailboder')
            }
        }
    }
</script>

<style scoped>
    .play-count{
        position: absolute;
        top: 2rem;
        left: 2rem;
        color: white
    }
    .classify_img{
        width: 100%;
        height: 15rem;
        border-radius: 0.9375rem;
        padding-top: 10px;
    }
    .state {
        position: absolute;
        right: -0.5rem;
        top: -2.5rem;
        color: #637777;
    }

    .title_cloum {
        display: flex;
        align-items: center;
        padding: 0.5rem 0.5rem;
        font-size: 1.25rem;
        position: relative;
    }

    .title-right {
        position: absolute;
        /* top:4rem;
        right:3rem; */
        top: 50%;
        left: 60%;
        transform: translate(-50%, -40%);
    }

    .title-left {
        width: 48%;
        height: 10rem;
        display: flex;
        flex-direction: column;
        display: inline-block;
    }

    .title-span {
        width: 100%;
        height: 14rem;
        /* line-height: 15.625rem; */
        padding: 1rem 1.25rem;
        position: relative;
        margin-bottom: 2rem;
        border-top: 1px solid #000000;
        border-bottom: 1px solid #000000;
        border-radius: 1.5rem;
        box-shadow: 1px 5px 5px #7cb0b1;
    }

    .title-img {
        width: 8rem;
        height: 8rem;
        border-radius: 1rem;
    }

    .playcount {
        position: absolute;
        top: 4.625rem;
        left: 2rem;
        color: white
    }

    .classify-span {
        padding: 0px 1rem;
        position: relative;

    }

    .classify-img {
        width: 100%;
        height: 7rem;
        border-radius: 0.9375rem;
    }

    .classify {
        width: 100%;
        height: 8rem;
    }

    .title {
        width: 100%;
        height: 3rem;
        line-height: 3rem;
        font-size: 1.6rem;
        font-weight: 600;
        letter-spacing: 0.5rem;
    }

    .flex {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .header {
        width: 100%;
        height: 3.125rem;
        background-color: white
    }

    .header-left {
        width: 40%;
        font-size: 1.3rem;
    }

    .header-right {
        padding-right: 0.625rem;
    }
</style>
  • DetailBoder.vue 排行榜详情,放在Home文件夹下,效果如图

<template>
    <div>
            <div class="itemMusictop">
                <div class="itemleft">
                    <van-icon name="arrow-left" size="25px" color="black" @click="$router.go(-1)" />
                </div>
                <div class="itemright">
                    <van-icon name="search" color="black" size="30px" @click="$router.push('/search')"/>
                    <van-icon name="more-o" color="black" size="30px" />
                </div>
            </div>
        <img :src="this.$route.params.img" alt="" class="bgimg">
        <div class="middle flex">
            <span>
                <van-icon name="add-o" size="30px" />{{this.$route.params.playcount|playcount-filter}}<span class="a"></span></span>
            <span>
                <van-icon name="chat-o" size="30px" />{{this.$route.params.subscribedCount|playcount-filter}}<span class="a"></span></span>
            <span>
                <van-icon name="exchange" size="30px" />{{this.$route.params.trackCount|playcount-filter}}</span>
        </div>

        <div class="listtop">
            <div class="listtop-left">
                <van-icon name="play-circle" size="35px" color="red" />
                <span>
                    <span class="listen-all">播放全部</span>
                    <span>(共{{list.length}}首)</span>
                </span>
            </div>
            <div class="listtop-right flex">
                <van-icon name="down" size="30px" />
                <van-icon name="coupon-o" size="30px" />
            </div>
        </div>

        <div class="itembottom">
            <div v-for="(item,index) in list.slice(0,40)" :key="item.id" @click="playmusic(index)">
                <div class="music-detail">
                    <div class="detail-left">
                        <span style=" font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
                        <span class="singer">
                            <span class="tomusic">{{item.al.name}}</span>
                            <span class="writer-music" style="">{{item.ar[0].name}}</span>
                        </span>
                    </div>
                    <van-icon name="bars" size="30px" />
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import Vue from 'vue'
    import axios from 'axios'
    import {mapState,mapMutations,mapAction } from 'vuex'
    Vue.filter('playcount-filter', function(data) {
        if (data >= 100000000) {
            return (data / 100000000).toFixed(1) + "亿"
        } else if (data >= 10000) {
            return (data / 10000).toFixed(2) + "万"
        } else if (data < 1000) {
            return data
        }
    })
    export default {
        data() {
            return {
                list: []
            }
        },
        created() {
            console.log(this.$route.params.id)
            axios.get(`http://localhost:3000/playlist/detail?id=${this.$route.params.id}`).then(res => {
                console.log(res)
                this.list = res.data.playlist.tracks
                console.log(this.list)
            })
        },
        methods:{
            playmusic:function(i){
                this.updatePlayList(this.list)
                this.updateIndex(i)
            },
            ...mapMutations(["updatePlayList","updateIndex"])
        }
    }
</script>

<style scoped>
    .itembottom {
        position: absolute;
        top:22rem;
        display:flex;
        flex-direction: column;
        width:100%;
    }

    .writer-music {
        font-size: 10px;
        color: #65737E;
    }

    .tomusic {
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
        font-size: 1.3rem;
        width: 10rem;
    }

    .detail-left {
        width: 60%;
        display: flex;
        justify-content: space-around;
    }

    .singer {
        display: flex;
        flex-direction: column;
    }

    .music-detail {
        display: flex;
        justify-content: space-around;
        align-items: center;
        height: 3.75rem;
    }

    .listtop-right {
        width: 30%;
    }

    .listen-all {
        font-size: 19px;
        font-weight: 800;
    }

    .listtop-left {
        display: flex;
        width: 45%;
        justify-content: space-between;
    }

    .listtop {
        display: flex;
        justify-content: space-around;
        height: 3rem;
        width: 100%;
        align-items: center;
        font-size: 1.0625rem;
        line-height: 3rem;
        position: absolute;
        top: 18rem;
        border-bottom: 1px solid #000000;
    }

    .a {
        padding-left: 0.9rem;
        border-right: 1px solid #000000;
    }

    .middle {
        width: 20rem;
        height: 4rem;
        border-top: 1px solid #000000;
        border-bottom: 1px solid #000000;
        border-radius: 1.5rem;
        box-shadow: 10px 5px 5px #b4ae8b;
        position: absolute;
        top: 26%;
        left: 50%;
        transform: translate(-50%, -50%);
        background-color: #FFFFFF;
    }

    .flex {
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .itemMusictop {
        width: 100%;
        height: 2.5rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
        line-height: 2.5rem;
        font-size: 18px;
        position: relative;
    }

    .itemright {
        width: 25%;
        height: 100%;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .itemleft {
        width: 10%;
        padding-left: 0.625rem;
    }

    .bgimg {
        width: 100%;
        height: 15rem;
        z-index: -1;
        position: absolute;
        top: 0px;
    }
</style>
  1. DetailMusic.vue 歌单详情,放在Home文件夹下,效果如图

<template>
    <div>
        <!-- 顶部 -->
        <van-sticky>
        <div class="itemMusictop">
            <!-- 虚化部分 -->
            <img :src="list.coverImgUrl" alt="" class="bgimg">

            <div class="itemleft">
                <van-icon name="arrow-left" size="25px" color="white" @click="$router.back()" />
                <span class="gedan" style="color:white">歌单</span>
            </div>
            <div class="itemright">
                <van-icon name="search" color="white" size="30px" @click="$router.push('/search')"/>
                <van-icon name="more-o" color="white" size="30px" />
            </div>
        </div>
        </van-sticky>

        <div class="itemmidle">
            <!-- 中间图片 -->
            <img :src="list.coverImgUrl" alt="" class="midleimg">
            <!-- 中间右半部分 -->
            <div class="midle-right">
                <span class="content">&nbsp;&nbsp;&nbsp;{{this.list.description}}</span>
                <span class="writer">
                    <img :src="list.coverImgUrl" alt="" class="left-img">
                    <span class="count">
                        <van-icon name="play-circle-o" size="30px" />
                        {{this.list.playCount|playcount-filter}}
                    </span>
                </span>
                <span class="item-type" style="color:black">{{this.list.name}}</span>
            </div>
        </div>
        <!-- tabar -->
        <div class="itemtabar">
            <div class="iconlist">
                <div class="iconitem" @click="comment()">
                    <van-icon name="comment-o" :badge="list.commentCount" size="30px" color="black" />
                    <span>评论</span>
                </div>
                <div class="iconitem">
                    <van-icon name="share-o" :badge="list.shareCount" size="30px" color="black" />
                    <span>分享</span>
                </div>
                <div class="iconitem">
                    <van-icon name="down" size="30px" color="black" />
                    <span>下载</span>
                </div>
                <div class="iconitem">
                    <van-icon name="diamond-o" size="30px" color="black" />
                    <span>多选</span>
                </div>
            </div>
        </div>

        <!-- 歌曲列表 -->
        <div class="musiclist">
            <div class="listtop">
                <div class="listtop-left">
                    <van-icon name="music-o" size="32px"  />
                    <span>
                        <span class="listen-all">播放全部</span>
                        <span>(共{{music.length}}首)</span>
                    </span>
                </div>
                <van-button type="warning" size="normal" :round="true" class="collection">
                    <van-icon name="plus" size="20px" />
                    收藏
                </van-button>
            </div>
            <div style="padding-bottom:80rem;height:76.25rem">
            <div v-for="(item,index) in music" :key="item.id">
                <div class="music-detail">
                    <div class="detail-left" @click="playmusic(index)">
                        <span style="font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
                        <span class="singer">
                            <span class="tomusic">{{item.al.name}}</span>
                            <span class="writer-music" style="">{{item.ar[0].name}}</span>
                        </span>
                    </div>
                    <img src="../../assets/play.png" alt="" class="play">
                    <van-icon name="bars" size="30px"/>
                </div>
            </div>
        </div>
        </div>
    </div>
</template>

<script>
    import axios from 'axios'
    import Vue from 'vue'
    import {mapState,mapMutations,mapAction } from 'vuex'
    Vue.filter('playcount-filter', function(data) {
        if (data >= 100000000) {
            return (data / 100000000).toFixed(1) + "亿"
        } else if (data >= 10000) {
            return (data / 10000).toFixed(2) + "万"
        }
    })
    export default {
        data() {
            return {
                list: {},
                music: [],
                nickname:""
            }
        },
        created(){
            // console.log('created', this.$route.params.id)
            axios.get(`http://localhost:3000/playlist/detail?id=${this.$route.params.id}`)
                .then((item) => {
                    // console.log(item)
                    this.list = item.data.playlist
                    console.log(this.list)
                    this.nickname=this.list.creator.nickname
                })
            axios.get(`http://localhost:3000/playlist/track/all?id=${this.$route.params.id}&limit=20&offset=1`)
                .then((item) => {
                    this.music = item.data.songs
                    // console.log(this.music)
                    // console.log(this.list.commentCount)
                })
        },
        components: {},
        methods:{
            playmusic:function(i){
                this.updatePlayList(this.music)
                this.updateIndex(i)
            },
            // 传歌单id给commoent组件
            comment(){
                this.$router.push({name:'commoent',params:{id:this.$route.params.id,count:this.list.commentCount,coverImgUrl:this.list.coverImgUrl,
                name:this.list.name,nickname:this.nickname}})
            },
            ...mapMutations(["updatePlayList","updateIndex"])
        }
    }
</script>

<style scoped="">
    .play{
        width:1.8rem;
        height:1.8rem;
        display:none;
    }
    .music-detail:hover .play{
        display:inline-block;
    }
    .writer-music {
        font-size: 10px;
        color: #65737E;
    }

    .tomusic {
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
        font-size: 1.3rem;
        width: 10rem;
    }

    .detail-left {
        width: 60%;
        display: flex;
        justify-content: space-around;
    }

    .singer {
        display: flex;
        flex-direction: column;
        padding-top: 0.625rem;
    }

    .music-detail {
        display: flex;
        justify-content: space-between;
        align-items: center;
        height:3.75rem;
    }

    .listen-all {
        font-size: 19px;
        font-weight: 800;
    }

    .listtop-left {
        display: flex;
        width: 45%;
        justify-content: space-between;
    }

    .listtop {
        display: flex;
        justify-content: space-around;
        height: 2.5rem;
        width: 100%;
        align-items: center;
        font-size: 1.0625rem;
        line-height: 2.5rem;
    }

    .musiclist {
        width: 100%;
        height: 35rem;
        background-color: #fff;
        border-top-left-radius: 1rem;
        border-top-right-radius: 1rem;
        position: absolute;
        top: 21rem;
    }

    .iconlist {
        width: 100%;
        height: 2.5rem;
        line-height: 2.5rem;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .iconitem {
        width: 25%;
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding-top: 0.5rem;
    }

    .count {
        height: 1.875rem;
        line-height: 1.875rem;
        margin-left: 0.625rem;
        color: black;
    }

    .item-type {
        position: absolute;
        top: 6.5rem;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
    }

    .itemMusictop {
        width: 100%;
        height: 2.5rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
        line-height: 2.5rem;
        font-size: 18px;
        position: relative;
    }

    .itemleft,
    .itemright {
        width: 25%;
        height: 100%;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .bgimg {
        width: 100%;
        height: 9rem;
        position: fixed;
        z-index: -1;
        filter: blur(1.5rem);
    }

    .itemmidle {
        width: 100%;
        height: 13rem;
    }

    .midleimg {
        width: 9rem;
        height: 9rem;
        position: absolute;
        margin-top: 2.5rem;
        margin-left: 1.25rem;
        border-radius: 0.9375rem;
    }

    .content {
        width: 14rem;
        height: 2.5rem;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        color:black
    }

    .midle-right {
        position: absolute;
        margin: 3rem 0;
        right: 0.625rem;
    }

    .left-img {
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 50%;
    }

    .writer {
        height: 3rem;
        display: flex;
        align-items: center;
        position: absolute;
        top: 3rem;
        line-height: 3rem;
    }
</style>

DetailMusic.vue所包含的子组件

  • Commoent.vue放在Home文件夹下,展示歌单评论,效果如图

<template>
    <div>
        <!-- 头部 -->
        <van-sticky>
        <div class="header flex">
            <div class="header-left flex">
                <van-icon name="arrow-left" size="30px" color="black" @click="$router.push('/look')" />
                <span style="letter-spacing: 0.2rem;">评论({{this.$route.params.count}})</span>
            </div>
            <div class="header-right">
                <van-icon name="guide-o" size="40px" color="black" />
            </div>
        </div>
        </van-sticky>
        <!-- 中间 -->
        <div class="middle flex">
            <div class="middle-left">
                <img :src="this.$route.params.coverImgUrl" alt="">
            </div>
            <div class="middle-right">
                <span>{{this.$route.params.name}}</span>
                <span style="padding-top: 0.5rem;">by——{{this.$route.params.nickname}}</span>
            </div>
            <div class="go">
                <van-icon name="arrow" size="30px" />
            </div>
        </div>
        <div class="hairlin"></div>

        <!-- 评论区 -->
        <div class="comment">
            <div class="comment-header flex-around">
                <div class="comment_header_left">
                    <span>评论区</span>
                </div>
                <div class="comment_header_right flex-around">
                    <span class="active" id="recommend" @click="change">推荐</span>
                    <span id="hot" @click="change">最热</span>
                    <!-- <span id="new">最新</span> -->
                </div>
            </div>
        </div>

        <div class="detail" v-if="isshow">
            <div class="user" v-for="(item,index) in list" :key="index">
                <div class="user-header flex">
                    <div class="user_header_left flex-around">
                        <!-- 头像 -->
                        <img :src="item.user.avatarUrl" alt="">
                        <div class="user-nickname">
                            <!-- 网名 -->
                            <span class="nick_name">{{item.user.nickname}}</span>
                            <!-- 时间 -->
                            <span>{{item.timeStr}}</span>
                        </div>
                    </div>
                    <div class="user_header_right">
                        <!-- 点赞 -->
                        <van-icon name="thumb-circle-o" size="35px" color="red" />
                    </div>
                </div>
                <div class="user_content">
                    <!-- 评论     -->
                    <span class="content">{{item.content}}</span>
                </div>
                <!-- 地点 -->
                <!--                 <span>{{item.ipLocation.location}}</span> -->
            </div>
        </div>
        
        <div class="detail" v-if="!isshow">
            <div class="user" v-for="(item,index) in hot" :key="index">
                <div class="user-header flex">
                    <div class="user_header_left flex-around">
                        <!-- 头像 -->
                        <img :src="item.user.avatarUrl" alt="">
                        <div class="user-nickname">
                            <!-- 网名 -->
                            <span class="nick_name">{{item.user.nickname}}</span>
                            <!-- 时间 -->
                            <span>{{item.timeStr}}</span>
                        </div>
                    </div>
                    <div class="user_header_right">
                        <!-- 点赞 -->
                        <van-icon name="thumb-circle-o" size="30px" color="red" />
                        <span>{{item.likedCount}}</span>
                    </div>
                </div>
                <div class="user_content">
                    <!-- 评论     -->
                    <span class="content">{{item.content}}</span>
                </div>
                <!-- 地点 -->
                <!--                 <span>{{item.ipLocation.location}}</span> -->
            </div>
        </div>
    </div>
</template>

<script>
    import axios from 'axios'
    export default {
        data() {
            return {
                list: [],
                hot:[],
                isshow:true
            }
        },
        created() {
            axios.get(`http://localhost:3000/comment/playlist?id=${this.$route.params.id}&limit=30`).then(res => {
                this.list = res.data.comments
                // console.log(res)
                // console.log(this.list)
            }),
            axios.get(`http://localhost:3000/comment/hot?id=${this.$route.params.id}&type=2`).then(item=>{
                console.log(item)
                this.hot=item.data.hotComments
                console.log(this.hot)
            })
        },
        mounted() {
            
        },
        methods: {
            change(){
                var recommend=document.querySelector("#recommend")
                var hot=document.querySelector("#hot")
                if(this.isshow==true)
                {
                    recommend.classList.remove("active")
                    hot.classList.add("active")
                    this.isshow=false
                }
                else{
                    hot.classList.remove("active")
                    recommend.classList.add("active")
                    this.isshow=true
                }
            },
        },
    }
</script>

<style scoped>
    .active{
        font-weight: 800;
    }
    .detail {
        padding-bottom: 4.8rem;
    }

    .user_header_right {
        padding-right: 1.25rem;
    }

    .content {
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        text-indent: 2rem;
    }

    .user_content {
        padding: 0.625rem 3rem;
        border-bottom: 1px solid #bababa;
    }

    .user_header_left {
        width: 45%;
        padding-left: 1.25rem;
    }

    .user_header_left img {
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 50%;
    }

    .nick_name {
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
    }

    .user-nickname {
        width: 5.5rem;
        display: flex;
        flex-direction: column;
    }

    .user-header {
        width: 100%;
        height: 4.375rem;
    }

    .user {
        width: 100%;
    }

    .comment_header_right {
        width: 40%;
    }

    .comment-header {
        width: 100%;
        height: 3.125rem;
    }

    .flex-around {
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .hairlin {
        width: 100%;
        height: 0.625rem;
        background-color: #e4e4e3;
    }

    .middle-right {
        display: flex;
        flex-direction: column;
        padding-left: 0.625rem;
        font-size: 1.1rem;
    }

    .middle-left img {
        width: 5rem;
        height: 5rem;
        border-radius: 0.625rem;
    }

    .middle-left {
        width: 6rem;
        height: 100%;
        padding-left: 1.2rem;
        padding-top: 0.625rem;
    }

    .middle {
        width: 100%;
        height: 6rem;
    }

    .flex {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .header {
        width: 100%;
        height: 3.125rem;
        background-color: white
    }

    .header-left {
        width: 35%;
        font-size: 1.25rem;
    }

    .header-right {
        padding-right: 0.625rem;
    }
</style>
  1. FooterMusic.vue 放在views文件夹下,作为全局组件,固定于页面底部

<template>
    <div class="footermusic">
        <div class="footer-left" @click="showPopup">
            <img :src="list[playindex].al.picUrl" alt="" class="footer-img">
            <div>
                <p class="al-name">{{list[playindex].al.name}}</p>
                <span>横划切换上下首哦</span>
            </div>
        </div>

        <div class="footer-right">
            <van-icon name="play-circle-o" size="35px" @click="Play" id="musicicon" style="padding-right: 1.2rem;"/>
            <van-icon name="orders-o" size="35px"/>
        </div>
        
        <audio id="music" :src="`https://music.163.com/song/media/outer/url?id=${list[playindex].id}.mp3`"></audio>
    <van-popup  v-model="show" position="right" :style="{ height: '100%' ,width:'100%',color:'white'}" 
    closeable close-icon-position="top-left" 
    close-icon="arrow-left">
            <lyrics :musicList="list[playindex]" :Play="Play" :tem="tem" :islyric="islyric" :adduration="adduration"></lyrics>
        </van-popup>
    </div>
</template>

<script>
    import {mapState,mapMutations,mapAction } from 'vuex'
    import axios from 'axios'
    import Vue from 'vue'
    import lyrics from '@/views/LyRics.vue'
    
    export default {
        data() {
            return {
                tem:false,
                music:[],
                show:false,
                lyriclist:{},
                interval:0,
                islyric:false
            }
        },
        computed:{
            // 数据解构
            ...mapState(["list","playindex","detailshow",]),
        },
        mounted(){
            this.updatetime()
            // console.log(this.islyric)
        },
        updated(){
            axios.get(`http://localhost:3000/lyric?id=${this.list[this.playindex].id}`).then(res=>{
                this.lyriclist=res.data.lrc
                // console.log(this.lyriclist)
            }),
            this.adduration()
        },
        watch:{
            playindex:function(){
                var music=document.getElementById('music')
                var musicicon=document.getElementById('musicicon')
                music.autoplay="true"
                if(music.paused){
                    musicicon.className='van-icon van-icon-pause-circle-o'
                }
            }
        },
        methods:{
            Play(){
                var music=document.getElementById('music')
                var musicicon=document.getElementById('musicicon')
                // console.log(musicicon)
                // console.log('detail', this.$route.params.id)
                if(this.tem==false)
                {
                    music.play()
                    this.tem=true
                    // 歌词突出显示
                    this.updatetime()
                    musicicon.className='van-icon van-icon-pause-circle-o'
                }
                else
                {
                    music.pause()
                    this.tem=false
                    //清除定时器
                    clearInterval(this.interval)
                    musicicon.className='van-icon van-icon-play-circle-o'
                }
                console.log(this.tem)
            },
            showPopup(){
                this.show=true
                this.uplyricslist(this.lyriclist)
                // console.log(this.lyriclist)
            },
            // 计时器
            updatetime(){
                var music=document.getElementById('music')
                this.interval=setInterval(()=>{
                    this.upcurrentime(music.currentTime)
                },1000)
            },
            //歌曲总时长
            adduration(){
                var music=document.getElementById('music')
                this.upduration(music.duration)
            },
            // 方法解构
            ...mapMutations(["uplyricslist","upcurrentime","upduration"])
        },
        components:{
            lyrics,
        }
    }
</script>

<style scoped>
    .al-name{
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
    }
    .footer-right{
        width:20%;
        height:100%;
        display:flex;
        justify-content: space-around;
        align-items: center;
        z-index: 1000;
    }
    .footermusic {
        width: 100%;
        height: 4.5rem;
        position: fixed;
        bottom: 0px;
        border-top: 1px solid #000000;
        display: flex;
        padding: 0.8rem;
        justify-content: space-between;
        background-color:white;
    }
    .footer-left {
        width: 60%;
        height: 100%;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }
    .footer-img {
        width: 3rem;
        height: 3rem;
        border-radius: 50%;
        animation-name: music;
        animation-timing-function: linear; 
        animation-duration: 3s; 
        animation-iteration-count: infinite;
    }
    @keyframes music{
        0%{
            transform: rotate(0deg);
        }
        50%{
            transform: rotate(180deg);
        }
        100%{
            transform: rotate(365deg);
        }
    }
</style>

FooterMusic.vue的相关子组件

  • LyRics.vue 放在views文件夹下,效果如图

包含播放动画,进度条实时移动,歌词滚动功能

<template>
    <div>
        <img :src="musicList.al.picUrl" alt="" class="all-img">
        <div class="lyric-top">
            <div class="lyric-topleft">
                <div>
                    <!-- <marque :duration="600" :interval="2000">1111111111</marque> -->
                    <p style="color:white">{{musicList.al.name}}</p>
                    <span>
                        {{musicList.name}}
                    </span>
                    <van-icon name="arrow" />
                </div>
            </div>

            <div class="lyric-topright">
                <van-icon name="more-o" size="30px" color="white" />
            </div>
        </div>

        <div class="lyric-middle" v-show="!islyricshow" @click="islyricshow=true">
            <img src="../assets/needle-ab(1).png" alt="" 
            class="needle-img" :class="{needle_img_active:tem}">
            <img src="../assets/disc-plus(1).png" alt="" class="disc-img">
            <img :src="musicList.al.picUrl" alt=""  class="img-al" :class="{img_al_active:tem,img_al_pause:!tem}">
        </div>
        
        <div class="musiclyric" ref="musicallyric" v-show="islyricshow" @click="islyricshow=false">
            <p v-for="(item,index) in lyric" :key="index" 
            :class="{lyric_active:(currentime*1000>=item.time&&currentime*1000<item.pre),lyric:!(currentime*1000>=item.time&&currentime*1000<item.pre)}">
                {{item.lrc}}
            </p>
        </div>
        
        <div class="lyric-footer">
            <div class="footer-top footer">
                <van-icon name="like-o" size="35px" color="white" />
                <van-icon name="down" size="30px" color="white" />
                <van-icon name="good-job-o" size="30px" color="white" />
                <van-icon name="chat-o" size="30px" color="white" />
                <van-icon name="ellipsis" size="30px" color="white" />
            </div>
            <!-- 进度条 -->
            <div class="footer-content">
                <input type="range" class="range" min="0" :max="duration" v-model="currentime" step="0.05">
            </div>
            <div class="footer">
                <van-icon name="replay" size="30px" color="white" />
                <van-icon name="arrow-left" size="30px" color="white" @click="goPlay(-1)"/>
                <van-icon name="pause-circle-o" size="60px" color="white" v-if="tem" @click="Play"/>
                <van-icon name="play-circle-o" size="60px" color="white" v-else @click="Play"/>
                <van-icon name="arrow" size="30px" color="white" @click="goPlay(1)"/>
                <van-icon name="wap-home-o" size="30px" color="white" />
            </div>
        </div>
    </div>
</template>

<script>
    // import marque from 'vue-marquee';
    import {
        mapState,
        mapMutations,
        mapAction
    } from 'vuex'
    import Vue from 'vue'
    export default {
        props: ['musicList','tem','Play','adduration'],
        data(){
            return{
                // 是否显示歌词
                islyricshow:false,
            }
        },
        watch:{
            // 进度条
            currentime:function(newvalue){
                let p=document.querySelector("p.lyric_active")
                //拿到p中距离顶部的距离,offsetTop
                // console.log([p])
                if(p){
                    if(p.offsetTop>300)
                    {
                        this.$refs.musicallyric.scrollTop=p.offsetTop-300
                    }
                }
                // 当进度条到达末尾时,
                if(newvalue===this.duration)
                {
                    // 如果是列表中最后一首歌,则跳回第一首
                    if(this.playindex==this.musicList.length-1)
                    {
                        this.updateIndex(0)
                        this.Play()
                    }
                    // 不是则下一首
                    else{
                        this.updateIndex(this.playindex+1)
                    }
                }
                
            }
        },
        computed:{
        ...mapState(["lyriclist","currentime","playindex","list","duration"]),
        lyric:function(){
            let arr;
            if(this.lyriclist.lyric){
                arr=this.lyriclist.lyric.split(/[(\r\n)\r\n]+/).map((item,i)=>{
                    let min=item.slice(1,3)
                    let sec=item.slice(4,6)
                    let mill=item.slice(7,10)
                    let lrc=item.slice(11,item.length)
                    // 总时长转为毫秒
                    let time=parseInt(min)*60*1000+parseInt(sec)*1000+parseInt(mill)
                    if(isNaN(Number(mill))){
                        mill=item.slice(7,9)
                        lrc=item.slice(10,item.length)
                        time=parseInt(min)*60*1000+parseInt(sec)*1000+parseInt(mill)
                    }
                    return {min,sec,mill,lrc,time}
                })
            }
            arr.forEach((item,i)=>{
                if(i==arr.length-1||isNaN(arr[i+1].time)){
                    item.pre=10000
                }
                else{
                    item.pre=arr[i+1].time
                }
            })
            return arr
        }
        
        },
        mounted() {
            this.adduration()
        },
        methods: {
            // 切换歌曲
            goPlay:function(num){
                let index=this.playindex+num
                if(index<0)
                {
                    index=this.list.length-1
                }
                else if(index==this.list.length)
                {
                    index=0
                }
                this.updateIndex(index)
                console.log(this.lyriclist)
            },
            ...mapMutations(["updateIndex"]),
        },
        components: {

        },
    }
</script>

<style>
    .range{
        width:100%;
        height:0.1875rem;
    }
    .lyric_active{
        font-size:1.5rem;
        font-weight: 900;
        color:white;
        margin-bottom: 1.875rem;
    }
    .lyric{
        margin-bottom: 1.875rem;
        color:#919191;
    }
    .musiclyric{
        width:100%;
        height:33rem;
        display:flex;
        flex-direction: column;
        align-items: center;
        margin-top: 1.25rem;
        overflow: scroll;
    }
    .lyric-footer{
        position: fixed;
        bottom:0.625rem;
        width:100%;
    }
    .footer-content {
        width: 100%;
        height: 7rem;
        line-height: 7rem;
    }

    .footer {
        width: 100%;
        height: 3rem;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }

    .footer-top {
        margin-top: 7.4rem;
    }

    .img-al {
        width: 13rem;
        height: 13rem;
        border-radius: 50%;
        position: absolute;
        bottom: 3.14rem;
        z-index: -1;
        animation: rotate 10s linear infinite;
    }
    .img_al_active{
    animation-play-state: running;
    }
    .img_al_pause{
        animation-play-state:paused;
    }
    .disc-img {
        width: 20rem;
        height: 20rem;
        position: absolute;
        bottom: 0rem;
        animation: rotate 10s linear infinite;
    }
    .disc_img_active{
        animation-play-state: running;
    }
    .disc_img_pause{
        animation-play-state:paused;
    }
    @keyframes rotate{
        0%{
            transform:rotateZ(0deg) ;
        }
        100%{
            transform:rotateZ(360deg) ;
        }
    }
    .needle-img {
        width: 7rem;
        height: 13rem;
        position: absolute;
        left: 46%;
        transform: rotate(-10deg);
        transform-origin: 0 0;
        transition: all 2s;
        z-index: 10;
    }
    
    .needle_img_active {
        width: 7rem;
        height: 13rem;
        position: absolute;
        left: 46%;
        transform: rotate(6deg);
        transform-origin: 0 0;
        transition: all 2s;
        z-index: 10;
    }
    .lyric-middle {
        width: 100%;
        height: 30rem;
        display: flex;
        flex-direction: column;
        align-items: center;
        position: relative;
    }

    .all-img {
        width: 100%;
        height: 100%;
        position: absolute;
        z-index: -1;
        filter: blur(5rem);
    }

    .back {
        width: 1.875rem;
        height: 1.875rem;
    }

    .lyric-top {
        height: 4.5rem;
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .lyric-topleft {
        width: 60%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-left:4rem;
    }

    .lyric-topright {
        padding-right: 0.8rem;
    }
</style>
  1. Cloud.vue 云村,放在views文件夹下

  • 功能还未完善,如果不写的话,需将router中的相关配置代码和TaBar中的声明导航代码删除

<template>
    <div>
        云村
    </div>
</template>

<script>
</script>

<style>
</style>
  1. ShiPing.vue 视频,放在views文件夹下

  • 功能还未完善,如果不写的话,需将router中的相关配置代码和TaBar中的声明导航代码删除

<template>
    <div>
        视频
    </div>
</template>

<script>
</script>

<style>
</style>
  1. Login.vue 登录,放在views文件夹下

  • 网易云手机登录功能调用登录接口的速度比调用其他接口慢,所以登录功能一般不成功,但可以用游客登录

<template>
    <div>
        <div class="container" id="container">
            <div class="form-container sign-up-container">
                <!-- 注册 -->
                <form action="#">
                    <!-- <img :src="qrimgs" alt="" style="width:200px;height:200px"> -->
                </form>
            </div>
            <div class="form-container sing-in-container">
                <form action="#">
                    <input type="text" placeholder="请输入手机号" class="pwd" v-model="phone">
                    <input type="password" placeholder="请输入验证码" class="pwd" v-model="captcha">
                    <div class="check">
                        <span class="right"><a href="" @click="Captcha">短信验证码登录</a></span>
                        <span class="left"><a href="">用户注册</a></span>
                    </div>
                    <button @click="checkcaptcha">登陆</button>
                    <button @click="visitor">游客登陆</button>
                </form>
            </div>
            <div class="overlay-container">
                <div class="overlay">
                    <div class="overlay-pannel overlay-left">
                        <h1>知道账号?</h1>
                        <p>快快去登陆吧。</p>
                        <button class="ghost" id="signIn" @click="signInButton">登陆</button>
                    </div>
                    <div class="overlay-pannel overlay-right">
                        <h1>忘记帐号?</h1>
                        <p>点击扫码登录吧!</p>
                        <button class="ghost" id="signUp" @click="signUpButton">扫码登录</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import {
        mapState,
        mapMutations,
        mapAction
    } from 'vuex'
    import Vue from 'vue'
    import axios from 'axios'
    export default {
        data() {
            return {
                phone: "",
                captcha: "",
                uid:""
            }
        },
        computed: {
            ...mapState(["isfootermusic", "islogin"])
        },
        mounted() {

        },
        created() {},
        methods: {
            signUpButton() {
                var signUpButton = document.querySelector('#signUp');
                var container = document.querySelector('#container');
                container.classList.add('right-panel-active')
            },
            signInButton() {
                var signInButton = document.querySelector('#signIn');
                var container = document.querySelector('#container');
                container.classList.remove('right-panel-active')
            },
            // 判断登录成功
            success(res){
                // 表示登录成功
                if (res.data.code === 200) {
                    this.upchangelogin()
                    // 和方法解构一样
                    // this.$store.commit("uptoken",res.data.)
                    this.$router.push('/inforuser')
                    this.upchangefooter()
                } else {
                    alert("账号或密码不正确")
                    this.password = ""
                }
            },
            // 验证码登录
            login(res) {
                if(res.data.data){
                    axios.get(`http://localhost:3000/login/cellphone?phone=${this.phone}&captcha=${this.captcha}`).then(item => {
                        console.log(item)
                    })
                }
            },
            // 获取验证码
            Captcha() {
                axios.get(`http://localhost:3000/captcha/sent?phone=${this.phone}`).then(res => {
                    console.log(res)
                })
            },
            // 判断验证码是否正确
            checkcaptcha(){
                axios.get(`http://localhost:3000/captcha/verify?phone=${this.phone}&captcha=${this.captcha}`).then(res=>{
                    console.log(1111,res)
                    this.login(res)
                })
            },
            // 游客登录
            visitor(){
                axios.get(`http://localhost:3000/register/anonimous`).then(res=>{
                    console.log(res)
                    this.success(res)
                    this.getuser(res.data.userId)
                })
            },
            // 获取用户信息
            getuser(uid){
                axios.get(`http://localhost:3000/user/account`).then(res=>{
                    console.log(res)
                })
            },
            ...mapMutations(["upchangefooter", "upchangelogin"])
        },
        watch: {
        },

    }
</script>

<style scoped>
    * {
        margin: 0;
        padding: 0;
        /*标准盒子 */
        box-sizing: border-box;
    }

    body,
    html {
        background: #f8f8f8;
        /* 锁定背景:*/
        background-attachment: fixed;
        /* 弹性布局 */
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: 100%;
        margin: 0 auto;
    }

    h1 {
        margin: 15px;
        font-size: 20px;
    }

    p {
        font-size: 1rem;
        line-height: 1.5rem;
        /* 字体变淡 */
        font-weight: 100;
        margin: 1.2rem 0;
        /* 字间距 */
        letter-spacing: 0.1rem;
    }

    span {
        font-size: 0.8rem;
        margin: 1.2rem 0;
    }

    a {
        color: #333;
        font-size: 1rem;
        /* 下划线消失 */
        text-decoration: none;
    }

    .left {
        float: left;
    }

    .check {
        display: flow-root;
        width: 80%;
    }

    .container {
        /* 相对定位 */
        position: absolute;
        background: #fff;
        border-radius: 10px;
        box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
        padding: 0.6rem;
        width: 50rem;
        height: 28rem;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        /* 溢出隐藏 */
        overflow: hidden;
        max-width: 100vw;
        min-height: 70vh;
    }

    .form-container form {
        background: #fff;
        /* 弹性布局 */
        display: flex;
        flex-direction: column;
        padding: 0 1.8rem;
        height: 100%;
        justify-content: center;
        align-items: center;
    }

    .social-container {
        margin: 0.6rem 0;
    }

    .social-container a {
        border: 1px solid #eee;
        border-radius: 50%;
        display: inline-flex;
        justify-content: center;
        align-items: center;
        margin: 0 5px;
        height: 1.8rem;
        width: 1.8rem;
    }

    .social-container a:hover {
        opacity: 0.8;
    }

    .form-container input {
        width: 100%;
        height: 2.2rem;
        text-indent: 1rem;
        border: 1px solid #ccc;
        /* 把input上左右边框取消 */
        border-left: none;
        border-right: none;
        border-top: none;
        /* 点击input边框消失 */
        outline: none;
        margin: 0.6rem 0;
    }

    /* 被选中时候缩小 */
    .form-container button:active {
        transform: scale(0.95, 0.95);
    }

    .form-container button {
        padding: 0.4rem 1rem;
        background: #b2e4ff;
        color: white;
        border: 1px solid #fff;
        outline: none;
        /* 鼠标放上变小手 */
        cursor: pointer;
        width: 100%;
        border-radius: 8px;
        transition: all 100ms ease-in;
        margin: 0.6rem 0;
        font-size: 0.6rem;
        padding: 0.5rem 0;
    }

    button#send_code {
        width: 40%;
        position: absolute;
        top: 53%;
        right: 20px;

    }

    button.ghost {
        background: transparent;
        border-color: #fff;
        border: 1px solid #fff;
        outline: none;
        cursor: pointer;
        width: 60%;
        border-radius: 8px;
        transition: all 800ms ease-in;
        margin: 0.6rem 0;
        padding: 0.5rem 0;
        color: white;
        font-size: 0.6rem;
    }

    button.ghost:active {
        transform: scale(0.95, 0.95);
    }

    .pwd {
        padding: 5px 9%;
        background-size: 21px 21px;
        background-position: 3% 10%;
    }

    .form-container {
        /* 绝对定位 */
        position: absolute;
        top: 0;
        height: 100%;
        transition: all 0.5s ease-in;

    }

    .sing-in-container {
        left: 0;
        width: 50%;
        z-index: 2;
    }

    .sign-up-container {
        left: 0;
        width: 50%;
        opacity: 0;
        z-index: 1;
    }

    .overlay {
        background: #b2e4ff;
        width: 200%;
        height: 100%;
        position: relative;
        left: -100%;
        transition: all 0.6s ease-in-out;
        color: white;
    }

    .overlay-container {
        position: absolute;
        top: 0;
        right: 0;
        width: 50%;
        height: 100%;
        overflow: hidden;
        transition: all 0.6s ease-in-out;
        z-index: 99;
    }

    .overlay-pannel {
        position: absolute;
        top: 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        width: 50%;
        height: 100%;
        padding: 0 2.2rem;
    }

    .overlay-right {
        right: 0;
    }

    .container.right-panel-active .overlay-container {
        transform: translateX(-100%);
    }

    .container.right-panel-active .sing-in-container {
        transform: translateX(100%);
    }

    .container.right-panel-active .sign-up-container {
        transform: translateX(100%);
        opacity: 1;
        z-index: 5;
        transition: all 0.6s ease-in-out;
    }

    .container.right-panel-active .overlay {
        transform: translateX(50%);
    }

    .container.right-panel-active .overlay-left {
        transform: translateX(0);
        transition: all 0.6s ease-in-out;
    }

    .container.right-panel-active .overlay-right {
        transform: translateX(20%);
        transition: all 0.6s ease-in-out;
    }
</style>

Login.vue子组件

  • InforUser.vue 个人信息

  • 功能还未完善,如果不写的话,需将router中的相关配置代码和Login.vue success函数中的相关代码删除

<template>
    <div>
        个人信息
    </div>
</template>

<script>
</script>

<style>
</style>
  1. 最后,App.vue组件,也是根组件

<template>
    <div id="app">
        <router-view ></router-view>
        <down v-if="isfootermusic"></down>
    </div>
</template>

<style lang="scss">
    * {
        padding: 0px;
        margin: 0px;
        box-sizing: border-box;
    }

    a {
        color: black;
    }
</style>

<script>
    import Vue from 'vue'
    import Vant from 'vant'
    import down from '@/views/FooterMusic.vue'
    import MusicList from '@/views/Home/MusicList.vue'
    import {mapState,mapMutations,mapAction } from 'vuex'
    import 'vant/lib/index.css'
    Vue.use(Vant)
    export default {
        components: {
            down,
            MusicList,
        },
        computed:{
            ...mapState(["isfootermusic"])
        }
    }
</script>
  1. 注意

  • 运行项目时,可以取消eslint规则的检查,防止代码书写不规范问题

  • 已经安装Vant,但是引用Vant里面组件库时候报错,可以重新安装vant2,并重启项目

npm i vant@latest-v2 -S
  1. 该项目也是我近几天所写,肯定还有许多需要完善的地方,还请各位多多指教,小妹谢谢各位咯!!!

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值