• Vue2 - 网易云音乐项目笔记(基于Vant UI组件库)


    一、项目技术

    基于 Vue + Vant UI组件库 实现网易云音乐
    Vant UI (有赞团队打造的移动端Vue组件库)地址:https://vant-contrib.gitee.io/vant/#/zh-CN

    共项目共实现四个功能:主页Home、搜索页Search、评论页Comment、播放页Play
    网易云手机端地址:https://y.music.163.com/m
    在这里插入图片描述

    二、准备工作

    1、初始化Vue项目

    ① 安装Vue脚手架

    npm i @vue/cli -g
    
    • 1

    ② 切换到要创建项目的目录,然后创建music项目

    vue create music
    
    • 1

    ③ 启动项目

    npm run serve
    
    • 1

    2、配置Vant UI组件库

    ① 下载vant ui库
    注意项:Vant组件库要下载配合Vue2版本的使用

    npm i vant@latest-v2
    
    • 1

    ② 配置按需引入样式
    在基于 vue-cli 的项目中使用 Vant 时,可以使用 unplugin-vue-components 插件,它可以自动引入组件,并按需引入组件的样式。
    安装插件

    npm i unplugin-vue-components -D
    
    • 1

    配置插件:在 vue.config.js 文件中配置插件

    const { VantResolver } = require('unplugin-vue-components/resolvers');
    const ComponentsPlugin = require('unplugin-vue-components/webpack');
    
    module.exports = {
      configureWebpack: {
        plugins: [
          ComponentsPlugin({
            resolvers: [VantResolver()],
          }),
        ],
      },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    优点:减少代码体积
    缺点:使用起来会变得繁琐一些

    3、下载并使用vue-router

    ① 下载vuex库(vue2中必须使用vue-router3版本)

    npm i vue-router@3
    
    • 1

    ② 引入

    import VueRouter from "vue-router"
    
    • 1

    ③ 使用VueRouter

    Vue.use(VueRouter)
    
    • 1

    ④ 使用VueRouter之后,在创建vm的时候就可以传入一个router配置项

    new Vue({
    	//使用VueRouter之后,创建vm的时候可以传入一个router配置项
        router,
        render:h=>h(App),
    }).$mount("#app")
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、接口API

    项目地址:https://neteasecloudmusicapi.js.org/#/
    文档地址:https://binaryify.github.io/NeteaseCloudMusicApi/#/
    ① 安装项目

    // 从github上克隆项目到本地
    git clone git@github.com:Binaryify/NeteaseCloudMusicApi.git
    
    // 安装依赖包
    npm install
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ② 运行项目

    node app.js
    
    • 1

    ③ 本地地址
    http://localhost:3000
    在这里插入图片描述

    5、postcss插件

    webpack的一个插件,将要px转化为rex插件
    中文官网:https://www.postcss.com.cn/
    ① 安装postcss

    npm install postcss
    
    • 1

    ② 安装postcss-pxtorem(最新版本6.0与Vant不兼容,因此安装5.1.1版本)
    postcss-pxtorem是postcss插件,把px转换成rem

    npm install postcss-pxtorem@5.1.1 --s-d
    
    • 1

    ③ 根目录下创建postcss.config.js文件

    module.exports = {
        plugins: {
            'postcss-pxtorem': {
                rootValue: 37.5, // 已设计稿宽度375px为例 vant用的是375的设计稿
                propList: ['*'],
            },
        },
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    三、分析页面实现功能

    分析:
    根据项目页面构成,我们要创建4个vue组件,分别是views/Home、views/Search、views/Comment、views/Play

    页面之间通过路由跳转,因此要创建文件夹 router/index.js

    页面所需要的数据是通过发送网络请求获取的,因此要创建untils/request.js文件,api/index.js作为统一出口

    首页和搜索页的顶部和底部一样,可以将这部分提取为一个组件,因此要创建views/layout.vue

    首页最新新闻和搜索页最佳匹配样式结构一样,因此也可以提取出来,创建components/songItem.vue组件

    1、路由页面准备

    layout属于一级路由,home和search属于二级路由;comment、play属于一级路由
    src/router/index.js文件代码

    import Vue from "vue";
    
    import VueRouter from "vue-router";
    
    import Layout from "@/views/Layout";
    import Home from "@/views/Home";
    import Search from "@/views/Search";
    import Play from "@/views/Play";
    import Comment from "@/views/Comment"
    
    Vue.use(VueRouter);
    
    export default new VueRouter({
        routes:[
            {
                path:"/",
                redirect:'/layout',
            },
            {
                path:"/layout",
                redirect:'/layout/home',
                component:Layout,
                children:[
                    {
                        name:'home',
                        path:'home',
                        component:Home,
                        meta:{
                            title:'首页'
                        }
                    },{
                        name:'search',
                        path:'search',
                        component:Search,
                        meta:{
                            title:'搜索'
                        }
                    }
                ]
            },
            {
                name:'play',
                path:'/play',
                component:Play,
            },
            {
                name:'comment',
                path:'/comment',
                component:Comment
            }
        ]
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    2、封装axios网路请求

    新建untils/request.js

    import axios from "axios";
    
    // axios.create()创建一个axios对象
    const request = axios.create({
        //基础路径,发请求的时候,路径当中会出现api,不用你手写
    	baseURL:'http://localhost:3000',
    	//请求时间超过5秒
    	timeout:5000
    });
    
    export default request
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3、公共组件

    components/songItem.vue组件

    <template>
            <van-cell :id="id" :title="name" :label="autor" @click="getComment(id)">
            <template #right-icon>
                <van-icon  name="play-circle-o" class="search-icon" @touchstart ="playFn(id)"/>
            template>
            van-cell>
    template>
    <script>
    
    export default {
        name:'SongItem',
        props:{name:String,autor:String,id:Number},
        methods:{
            playFn(id){
                this.$router.push({
                    name:'play',
                    query:{
                        id:id
                    }
                });
                console.log(id);
            },
            getComment(id){
                this.$router.push({
                    name:'comment',
                    query:{
                        id
                    }
                })
            }
        }
    }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    四、具体页面实现

    1、Layout组件相关

    views/Layout/index.vue

    <template>
        <div>
            
            <van-nav-bar :title="activeTitle" fixed/>
            
            <div class="main">
                
                <keep-alive include="Home,Search">
                    <router-view>router-view>
                keep-alive>
            div>
            
            <van-tabbar v-model="active">
                <van-tabbar-item icon="home-o" to="/layout/home">首页van-tabbar-item>
                <van-tabbar-item icon="search" to="/layout/search">搜索van-tabbar-item>
            van-tabbar>
        div>
    
    template>
    
    <script>
    import { ref } from 'vue';
    export default {
        name:'Layout',
        data(){
            return {
                activeTitle:this.$route.meta.title,
            }
        },
        setup() {
            const active = ref(0);
            return {
               active,
            };
        },
        // 通过监听路由切换
        watch:{
            $route(){
                this.activeTitle = this.$route.meta.title
            }
        }
    }
    script>
    <style scoped>
        .main {
            padding-top: 56px;
            padding-bottom: 66px;
        }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    2、Home组件相关

    ① 封装home相关axios请求

    api/home.js

    import request from "@/untils/request";
    
    // 推荐音乐
    export const hotMusic = (params)=>request({url:'/personalized',params})
    
    // 最新音乐
    export const newMusic = (params)=>request({url:'/personalized/newsong',params})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ② Home组件代码

    views/Home/index.vue

    <template>
      <div>
        <p class="title">推荐歌单p>
        <van-row gutter="10" >
            <van-col span="8" v-for="m in hotMusic" :key="m.id">
              <van-image width="100%" height="3rem" :src="m.picUrl"/>
              <p class="song_name">{{m.name}}p>
            van-col>
        van-row>
    
        <p class="title">最新音乐p>
        <SongItem v-for="n in newMusic" :key="n.id" :name="n.name" :autor="n.song.artists[0].name + n.name" :id="n.id">SongItem>
      div>
    template>
    <script>
    import SongItem from "@/components/songItem.vue";
    import {hotMusicAPI,newMusicAPI} from "@/api";
    export default {
      name:'Home',
      data(){
        return {
          hotMusic:[],
          newMusic:[],
        }
      },
      components:{SongItem},
      async created(){
        // 获取推荐歌曲
        const resHot= await hotMusicAPI({limit:6})
        this.hotMusic = resHot.data.result;
        // 获取最新音乐
        const resNew = await newMusicAPI({limit:20});
        this.newMusic = resNew.data.result;
      },
    }
    script>
    
    <style>
    /* 标题 */
    .title {
      padding: 0.266667rem 0.24rem;
      margin: 0 0 0.24rem 0;
      background-color: #eee;
      color: #333;
      font-size: 15px;
    }
    /* 推荐歌单 - 歌名 */
    .song_name {
      font-size: 0.346667rem;
      padding: 0 0.08rem;
      margin-bottom: 0.266667rem;
      word-break: break-all;
      text-overflow: ellipsis;
      display: -webkit-box; /** 对象作为伸缩盒子模型显示 **/
      -webkit-box-orient: vertical; /** 设置或检索伸缩盒对象的子元素的排列方式 **/
      -webkit-line-clamp: 2; /** 显示的行数 **/
      overflow: hidden; /** 隐藏超出的内容 **/
    }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    3、Search组件相关

    ① 封装search相关axios请求

    import request from "@/untils/request";
    
    //热词
    export const hotSearch = ()=>request({url:'/search/hot/detail'});
    
    // 关键词搜索
    
    export const keywordSearch = (params) => request({url:"/cloudsearch",params})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ②Search组件代码

    <template>
        <div>
            <van-search v-model="searchValue" placeholder="请输入搜索关键词" shape="round"/>
             
            <div class="search_wrap" v-if="searchValue.length==0">
                <p class="hot_title">热门搜索p>
                <div class="hot_name_wrap">
                    <span class="hot_item" v-for="h in hotSearch" :key="h.score" @click="btn(h.searchWord)">{{h.searchWord}}span>
                div>
            div>
            
            <div class="search_wrap" v-else>
                <p class="hot_title">最佳匹配p>
                 <SongItem v-for="k in keywordSearch" :key="k.id" :name="k.name" :autor="k.ar[0].name + ' / '+ k.al.name" :id="k.id">SongItem>
            div>
        div>
    template>
    <script>
    import {hotSearchAPI,keywordSearchAPI} from "@/api";
    import SongItem from "@/components/songItem.vue";
    export default {
        name:'Search',
        data() {
            return {
                searchValue:'', // 搜索框的值
                hotSearch:[],    // 热词
                keywordSearch:[]    //关键词搜索结果
            }
        },
        components:{SongItem},
        async created(){
            //获取热搜词
           const hotSearch = await hotSearchAPI();
           this.hotSearch = hotSearch.data.data;
        },
        methods:{
            async btn(str){
                this.searchValue = str;
                const keywordSearch = await keywordSearchAPI({keywords:this.searchValue,type:1});
                this.keywordSearch = keywordSearch.data.result.songs;
                //点击热词不需要等待立即执行
                setTimeout(()=>{
                    clearTimeout(this.timer);
                })
            },
        },
         watch:{
            // 通过监视searchValue的变化来获取输入框的内容
            searchValue(){
                if(this.searchValue.length==0) {
                    return this. keywordSearch = []
                }else{
                    //设置防抖减少向服务器发送请求。类似于王者荣耀回车
                    clearTimeout(this.timer);
                    this.timer = setTimeout(async()=>{
                        const keywordSearch = await keywordSearchAPI({keywords:this.searchValue,type:1});
                        this.keywordSearch = keywordSearch.data.result.songs;
                    },3000)
                }
            }
        }
    }
    script>
    <style scoped>
        /* 热门搜索容器的样式 */
        .search_wrap {
        padding: 0.266667rem;
        }
    
        /*热门搜索文字标题样式 */
        .hot_title {
        font-size: 0.32rem;
        color: #666;
        }
    
        /* 热搜词_容器 */
        .hot_name_wrap {
        margin: 0.266667rem 0;
        }
    
        /* 热搜词_样式 */
        .hot_item {
        display: inline-block;
        height: 0.853333rem;
        margin-right: 0.213333rem;
        margin-bottom: 0.213333rem;
        padding: 0 0.373333rem;
        font-size: 0.373333rem;
        line-height: 0.853333rem;
        color: #333;
        border-color: #d3d4da;
        border-radius: 0.853333rem;
        border: 1px solid #d3d4da;
        }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    4、Comment组件相关

    ① 封装comment相关axios请求

    import request  from "@/untils/request";
    
    // 获取评论
    export const getComment = (params) =>request ({url:"/comment/hot",params})
    
    • 1
    • 2
    • 3
    • 4

    ②Comment组件代码

    <template>
        <div>
            <van-nav-bar title="评论" fixed left-arrow @click-left="$router.back()"/>
            <div>
                <div class="main" >
                    
                    <van-pull-refresh v-model="refreshing" @refresh="onRefresh" success-text="刷新成功">
                        <van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
                            <van-cell v-for="(c,index) in list" :key="index">
                                <div class="wrap" >
                                    <div class="img_wrap">
                                        <img :src="c.user.avatarUrl" alt="">
                                    div>
                                    <div class="conent_wrap">
                                        <div class="header_wrap" >
                                            <div class="info_wrap">
                                                <p>{{c.user.nickname}}p>
                                                <p>{{c.time}}p>
                                            div>
                                            <div>{{c.likedCount}}点赞div>
                                        div>
                                        <div class="footer_wrap">
                                            {{c.content}}
                                        div>
    
                                    div>
                                div>
                            van-cell>
                        van-list>
                    van-pull-refresh>
    
                div>
            div>
        div>
    template>
    <script>
    import {getCommentAPI} from "@/api";
    export default {
        name:'Comment',
        data(){
            return {
                id : this.$route.query.id,
                commentsInfo:[], // 每次接收20个评论数据
                page:1, // 页码
                loading:false, // 下拉加载状态
                finished:false, // 所有数据是否加载完成状态
                refreshing:true, // 上拉加载状态
                list:[] // 所有数据
            }
        },
        methods: {
            //获取数据
            async getList(){
                const getComment = await getCommentAPI({id:this.id,type:0,limit:20,offset:(this.page -1 )*20});
                this.commentsInfo = getComment.data.hotComments;
                this.commentsInfo.forEach(obj=>this.list.push(obj))
                this.loading = false;
            },
    
            // 上拉刷新
            async onLoad(){
                console.log(this.list.length)
                        if(this.loading){
                         this.getList();
                         this.page++;
                         this.refreshing = false;
                }
    
                if(this.list.length %20 != 0) {
                    this.loading = false;
                    this.finished = true;
                }
            },
    
            // 下拉刷新
            async onRefresh(){
                this.finished = false;
                this.loading = true;
                this.onLoad();
            }
    
        },
    
    }
    script>
    
    <style scoped>
        .main {
            padding-top: 46px;
        }
        .wrap {
            display: flex;
        }
        .img_wrap {
            width: 0.8rem;
            height: 0.8rem;
            margin-right: 0.266667rem;
        }
        .img_wrap img {
            width: 100%;
            height: 100%;
            border-radius: 50%;
        }
        .conent_wrap {
            flex: 1;
        }
        .header_wrap {
            display: flex;
        }
        .info_wrap {
            flex: 1;
        }
        .info_wrap p:first-child {
            font-size: 0.373333rem;
            color: #666;
        }
        .info_wrap p:last-of-type {
            font-size: 0.24rem;
            color: #999;
        }
        .header_wrap div:last-of-type {
            color: #999;
            font-size: 0.293333rem;
        }
        .footer_wrap {
            font-size: 0.4rem;
            color: #333;
        }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129

    5、Play组件相关

    ① 封装play相关axios请求

    import request from "@/untils/request";
    
    //获取音乐播放地址
    export const getSongById = (params)=>request({url:"/song/url/v1",params});
    
    //获取歌词
    export const getLyricById = (params)=>request({url:'/lyric',params});
    
    //获取歌曲详情
    export const getMusicById = (params)=>request({url:'/song/detail',params})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ②Play组件代码

    <template>
      <div class="play">
        
        <div
          class="song-bg" :style="`background-image: url();`">div>
        
        <div class="header">
          <van-icon name="arrow-left" size="20" class="left-incon" @click="$router.back()"/>
        div>
        
        <div class="song-wrapper">
          
          <div class="song-turn ani" :style="`animation-play-state:${playState ? 'running' : 'paused'}`">
            <div class="song-img">
              
              <img class="musicImg"  :src="musicInfo.al.picUrl"/>
            div>
          div>
          
          <div class="start-box" @click="audioStart">
            <span class="song-start" v-show="!playState">span>
          div>
          
          <div class="song-msg">
            
            <h2 class="m-song-h2">
              <span class="m-song-sname">{{ musicInfo.name }}-{{musicInfo.ar[0].name}}span
              >
            h2>
            
            <div class="needle" :style="`transform: rotate(${needleDeg});`">div>
            
            <div class="lrcContent">
              <p class="lrc">{{ curLyric }}p>
            div>
          div>
        div>
        
         <audio ref="audio" preload="true" :src="songInfo.url" @timeupdate="timeupdate">audio>
      div>
    template>
    
    <script>
    // 获取歌曲详情和 歌曲的歌词接口
    import { getSongByIdAPI, getLyricByIdAPI,getMusicByIdAPI } from '@/api'
    export default {
      name: 'play',
      data() {
        return {
          playState: false, // 音乐播放状态(true暂停, false播放)
          id: this.$route.query.id, // 上一页传过来的音乐id
          songInfo: {}, // 歌曲信息
          musicInfo:"", // 歌曲详情信息
          lyric: {}, // 歌词枚举对象(需要在js拿到歌词写代码处理后, 按照格式保存到这个对象)
          curLyric: '', // 当前显示哪句歌词
          lastLy: '' ,// 记录当前播放歌词
        }
      },
    
      computed: {
        needleDeg() { // 留声机-唱臂的位置属性
          return this.playState ? '-7deg' : '-38deg'
        }
      },
      async created(){
       // 获取歌曲详情, 和歌词方法
          const res = await getSongByIdAPI({id:this.id})
          this.songInfo = res.data.data[0];
    
          // 获取歌曲详情
          const musicInfo =await getMusicByIdAPI({ids:this.id});
          this.musicInfo = musicInfo.data.songs[0];
    
          // 获取-并调用formatLyric方法, 处理歌词
          const lyrContent  = await getLyricByIdAPI({id:this.id});
          const lyricStr = lyrContent.data.lrc.lyric
          this.lyric = this.formatLyric(lyricStr)
           // 初始化完毕先显示零秒歌词
          this.curLyric = this.lyric[0]
    
      },
      methods: {
         formatLyric(lyricStr) {
          // 可以看network观察歌词数据是一个大字符串, 进行拆分.
          let reg = /\[.+?\]/g //
          let timeArr = lyricStr.match(reg) // 匹配所有[]字符串以及里面的一切内容, 返回数组
          console.log(timeArr); // ["[00:00.000]", "[00:01.000]", ......]
          let contentArr = lyricStr.split(/\[.+?\]/).slice(1) // 按照[]拆分歌词字符串, 返回一个数组(下标为0位置元素不要,后面的留下所以截取)
          console.log(contentArr);
          let lyricObj = {} // 保存歌词的对象, key是秒, value是显示的歌词
          timeArr.forEach((item, index) => {
            // 拆分[00:00.000]这个格式字符串, 把分钟数字取出, 转换成秒
            let ms = item.split(':')[0].split('')[2] * 60
            // 拆分[00:00.000]这个格式字符串, 把十位的秒拿出来, 如果是0, 去拿下一位数字, 否则直接用2位的值
            let ss = item.split(':')[1].split('.')[0].split('')[0] === '0' ? item.split(':')[1].split('.')[0].split('')[1] : item.split(':')[1].split('.')[0]
            // 秒数作为key, 对应歌词作为value
            lyricObj[ms + Number(ss)] = contentArr[index]
          })
          // 返回得到的歌词对象(可以打印看看)
          console.log(lyricObj);
          return lyricObj
        },
    
        // 监听播放audio进度, 切换歌词显示
        timeupdate(){
          // console.log(this.$refs.audio.currentTime)
          let curTime = Math.floor(this.$refs.audio.currentTime)
           // 避免空白出现
          if (this.lyric[curTime]) {
            this.curLyric = this.lyric[curTime]
            this.lastLy = this.curLyric
          } else {
            this.curLyric = this.lastLy
          }
        },
    
        // 播放按钮 - 点击事件
        audioStart() {
          if (!this.playState) { // 如果状态为false
            this.$refs.audio.play() // 调用audio标签的内置方法play可以继续播放声音
          } else {
            this.$refs.audio.pause() // 暂停audio的播放
          }
          this.playState = !this.playState // 点击设置对立状态
        },
      },
    }
    script>
    
    <style scoped>
    /* 歌曲封面 */
    .musicImg {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      width: 100%;
      margin: auto;
      width: 370rpx;
      height: 370rpx;
      border-radius: 50%;
    }
    
    /* 歌词显示 */
    .scrollLrc {
      position: absolute;
      bottom: 280rpx;
      width: 640rpx;
      height: 120rpx;
      line-height: 120rpx;
      text-align: center;
    }
    
    
    .header {
      height: 50px;
    }
    .play {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 1000;
    }
    .song-bg {
      background-color: #161824;
      background-position: 50%;
      background-repeat: no-repeat;
      background-size: auto 100%;
      transform: scale(1.5);
      transform-origin: center;
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      height: 100%;
      overflow: hidden;
      z-index: 1;
      opacity: 1;
      filter: blur(25px); /*模糊背景 */
    }
    .song-bg::before{ /*纯白色的图片做背景, 歌词白色看不到了, 在背景前加入一个黑色半透明蒙层解决 */
      content: " ";
      background: rgba(0, 0, 0, 0.5);
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom:0;
    }
    .song-wrapper {
      position: fixed;
      width: 247px;
      height: 247px;
      left: 50%;
      top: 50px;
      transform: translateX(-50%);
      z-index: 10001;
    }
    .song-turn {
      width: 100%;
      height: 100%;
      background: url("./img/bg.png") no-repeat;
      background-size: 100%;
    }
    .start-box {
      position: absolute;
      width: 156px;
      height: 156px;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .song-start {
      width: 56px;
      height: 56px;
      background: url("./img/start.png");
      background-size: 100%;
    }
    .needle {
      position: absolute;
      transform-origin: left top;
      background: url("./img/needle-ab.png") no-repeat;
      background-size: contain;
      width: 73px;
      height: 118px;
      top: -40px;
      left: 112px;
      transition: all 0.6s;
    }
    .song-img {
      width: 154px;
      height: 154px;
      position: absolute;
      left: 50%;
      top: 50%;
      overflow: hidden;
      border-radius: 50%;
      transform: translate(-50%, -50%);
    }
    .m-song-h2 {
      margin-top: 20px;
      text-align: center;
      font-size: 18px;
      color: #fefefe;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .lrcContent {
      margin-top: 50px;
    }
    .lrc {
      font-size: 14px;
      color: #fff;
      text-align: center;
    }
    .left-incon {
      position: absolute;
      top: 10px;
      left: 10px;
      font-size: 24px;
      z-index: 10001;
      color: #fff;
    }
    .ani {
      animation: turn 5s linear infinite;
    }
    @keyframes turn {
      0% {
        -webkit-transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(360deg);
      }
    }
    style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283

    6、API统一出口

    API/index.js

    // 统一出口
    
    import {hotMusic,newMusic} from "@/api/home";
    
    import {hotSearch,keywordSearch} from "@/api/search";
    
    import { getSongById,getLyricById,getMusicById} from "@/api/play";
    
    import {getComment} from "@/api/comment";
    
    //导出推荐歌单方法
    export const hotMusicAPI = hotMusic
    //导出新歌方法
    export const newMusicAPI = newMusic
    
    //导入热搜
    export const hotSearchAPI = hotSearch;
    //导入关键词搜索
    export const keywordSearchAPI = keywordSearch;
    
    //播放音乐
    export const getSongByIdAPI = getSongById;
    //获取歌词
    export const getLyricByIdAPI = getLyricById;
    //获取歌曲详情
    export const getMusicByIdAPI = getMusicById;
    
    //获取评论
    export const getCommentAPI = getComment;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    五、Play和Comment组件详解

    在做项目的过程中,感觉Play组件歌词同步逻辑有点复杂,还有Comment组件上拉和下拉刷新也有点难,因此专门写了两篇详解,可以看一下~~~
    Play组件详解:Play组件播放音乐并实现同步一次显示一行歌词
    Comment组件详解:Comment组件评论页上拉和下拉刷新

    觉得有用,记得点赞收藏哦~~

  • 相关阅读:
    【RTOS学习】单片机中的C语言
    Java高级技术之Gradle
    SpringBoot如何优雅关闭(SpringBoot2.3&Spring Boot2.2)
    【JavaSe】异常
    Ps:简单快速的主背分离方法
    LeetCode:二分查找
    js “2018/01-2018/12“转为“2018年01月-2018年12月“,字符串的替换和插入
    开源.NetCore通用工具库Xmtool使用连载 - 扩展动态对象篇
    神经网络物联网的发展趋势和未来方向
    C++ BinarySercahTree recursion version
  • 原文地址:https://blog.csdn.net/Vest_er/article/details/127216365