简介

  1. 这个项目仅用于练习vue,功能并未完善,bug也非常多,目前已完成主页、歌单列表、音乐播放、歌词;如果你想运行项目,需要下载musicapp与NeteaseCloudMusicApi-master,分别注入依赖后同时运行;素材不下载也没关系,并不妨碍运行项目;如果你希望加入新功能,可以点击下方文档
  2. 点我进入网易云音乐项目教学视频
  3. 点我进入网易云音乐NodeJS版API文档

    如果链接失效,谷歌搜索网易云音乐NodeJS版API

下载项目代码

  1. musicapp是项目代码,注入依赖后运行:npm run serve
  2. NeteaseCloudMusicApi-master是后端代码,配合上面文档使用,注入依赖后运行:node app.js
  3. 素材是新建项目时才会用到的图片、图标

    百度网盘:https://pan.baidu.com/s/1wYS3cYF8qROrPNjfeN2zag 提取码: nmsw

脚手架创建musicapp项目

  1. 创建musicapp项目
    1
    2
    3
    4
    5
    6
    7
    // 使用vue脚手架生成
    vue create musicapp

    // 多选router、vuex、less

    cd musicapp
    npm run serve

检测视窗宽度自适应字体大小

  1. public/js/rem.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const remSize = () => {
    // 获取视窗宽度
    let deviceWidth = document.documentElement.clientWidth || window.innerWidth;
    // 检测视窗宽度并设置最大值与最小值
    if(deviceWidth >= 750){
    deviceWidth = 750;
    }
    if(deviceWidth <= 320){
    deviceWidth = 320;
    }
    // 设计稿750px,那么1rem = 100px
    // 设计稿375px,那么1rem = 50px
    document.documentElement.style.fontSize = (deviceWidth / 7.5) + 'px';
    // 设置字体大小
    document.querySelector('body').style.fontSize = 0.3 + 'rem';
    }

    remSize()
    window.onresize = function (){
    remSize()
    }
  2. 将rem.js导入到public/index.html,位置在</body>的上面
    1
    2
    3
    4
    <body>
    ...
    <script src="<%= BASE_URL %>js/rem.js"></script>
    </body>

加载字体图标

  1. 字体图标放到src/assets,打开demo_index.html查看字体图标类名
  2. main.js导入import './assets/iconfont/iconfont.js'
  3. App.vue添加公共样式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <template>
    <router-view/>
    </template>

    <style lang="less">
    *{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: sans-serif;
    }
    .icon {
    width: 1rem;
    height: 1rem;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
    }
    </style>
  4. 测试字体图标能否显示,位置在HomeView.vue
    1
    2
    3
    4
    5
    6
    7
    <template>
    <div class="home">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-liebiao2"></use>
    </svg>
    </div>
    </template>

发现页面

顶部栏

  1. src/views/HomeView.vue导入TopNav组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div class="home">
    <!--顶部栏-->
    <top-nav></top-nav>
    </div>
    </template>

    <script>
    // @ is an alias to /src
    import TopNav from '@/components/HomeView/TopNav.vue'

    export default {
    name: 'HomeView',
    components: {
    TopNav
    }
    }
    </script>
  2. src/components/HomeView创建TopNav.vue组件

    个人习惯用大组件命名创建文件夹,在往里创建子组件,这样方便管理子组件

    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
    <template>
    <div class="topNav">
    <div class="topLeft">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-liebiao2"></use>
    </svg>
    </div>
    <div class="topCenter">
    <span class="navBtn">我的</span>
    <span class="navBtn active">发现</span>
    <span class="navBtn">云村</span>
    <span class="navBtn">视频</span>
    </div>
    <div class="topRight">
    <svg class="icon search" aria-hidden="true">
    <use xlink:href="#icon-sousuo"></use>
    </svg>
    </div>
    </div>
    </template>

    <script>
    export default {
    name: "TopNav"
    };
    </script>

    <style scoped lang="less">
    .topNav{
    width: 7.5rem;
    height: 1rem;
    display: flex;
    //首个元素在起点,末尾元素在终点
    justify-content: space-between;
    //垂直居中
    align-items: center;
    padding: 0 0.2rem;
    .icon{
    width: 0.4rem;
    height: 0.4rem;
    }
    .search{
    width: 0.47rem;
    height: 0.47rem;
    }
    .topCenter{
    width: 4.5rem;
    color: #C0C0C0;
    display: flex;
    // 平均分布
    justify-content: space-around;
    .active{
    color: black;
    font-weight: 900;
    }
    }
    }
    </style>

轮播图

  1. 安装swiper插件,实现触摸滑动功能npm install swiper@5 --save
  2. src/views/HomeView.vue导入Banner组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <template>
    <div class="home">
    <!--顶部栏-->
    <top-nav></top-nav>
    <!--轮播图-->
    <banner></banner>
    </div>
    </template>

    <script>
    // @ is an alias to /src
    import TopNav from '@/components/HomeView/TopNav.vue'
    import Banner from '@/components/HomeView/Banner.vue'

    export default {
    name: 'HomeView',
    components: {
    TopNav,
    Banner,
    },
    }
    </script>
  3. src/assets/img存放banner.webp,图片可以是电脑壁纸,大小尺寸无所谓
  4. src/components/HomeView创建Banner.vue组件
    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
    <template>
    <div id="swipercom">
    <div class="swiper-container" id="swiperIndex">
    <div class="swiper-wrapper">
    <!--循环获取轮播图-->
    <div class="swiper-slide" v-for="(item, i) in imgs" :key="i">
    <img :src="item.pic">
    </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>
    </div>
    </div>
    </template>

    <script>
    import 'swiper/css/swiper.css'
    import Swiper from 'swiper';
    import axios from 'axios';
    import { getBanner } from '@/api/index.js'

    export default {
    name: 'Banner',
    data(){
    return{
    // 至少准备1份本地图片,避免出现问题
    // require是动态获取本地图片
    // pic是后端请求banners的格式
    imgs:[
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    { pic: require('../assets/img/banner.webp') },
    ]
    }
    },
    async mounted(){
    // 异步请求轮播图,1是请求android资源类型
    let res = await getBanner(1);
    // 查看res数据
    // console.log(res)
    // 请求到的轮播图会替换imgs
    this.imgs = res.data.banners;
    const mySwiper = new Swiper('#swiperIndex', {
    loop: true, // 循环模式选项
    // 如果需要分页器
    pagination: {
    el: '.swiper-pagination',
    clickable: true,
    },
    });
    }
    };
    </script>

    <style lang="less">
    #swipercom{
    width: 7.5rem;
    #swiperIndex.swiper-container{
    width: 7.1rem;
    height: 2.6rem;
    border-radius: 0.1rem;
    .swiper-slide img{
    width: 100%;
    }
    .swiper-pagination-bullet-active{
    background-color: orangered;
    }
    }
    }
    </style>
  5. src/api创建index.js,用来管理请求数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import axios from 'axios';

    let baseUrl = 'http://localhost:3000'

    // 获取轮播图的api, type:资源类型, 对应以下类型, 默认为0即PC
    // 1: android; 2: iphone; 3: ipad
    export let getBanner = (type=0) => {
    return axios.get(`${baseUrl}/banner?type=${type}`)
    }
  6. 打开控制台,cd到NeteaseCloudMusicApi-master,运行node app.js,就能获取到后端发来的banner数据

图标列表

  1. src/views/HomeView.vue导入IconList组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--图标列表-->
    <icon-list></icon-list>

    import IconList from '@/components/HomeView/IconList.vue'

    components: {
    TopNav,
    Banner,
    IconList
    }
  2. src/components/HomeView创建IconList.vue组件
    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
    <template>
    <div class="iconList">
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-geinituijian"></use>
    </svg>
    <span>每日推荐</span>
    </div>
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-shouyinji"></use>
    </svg>
    <span>私人FM</span>
    </div>
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-yinlegedan-"></use>
    </svg>
    <span>歌单</span>
    </div>
    <div class="iconItem leaderboard">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-paihangbang-"></use>
    </svg>
    <span>排行榜</span>
    </div>
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-zhibo"></use>
    </svg>
    <span>直播</span>
    </div>
    </div>
    </template>

    <script>
    export default {
    name: "IconList"
    };
    </script>

    <style scoped lang="less">
    .iconList{
    display: flex;
    justify-content: space-around;
    padding: 0.4rem;
    .iconItem{
    display: flex;
    // 垂直布局
    flex-direction: column;
    // 垂直居中
    align-items: center;
    .icon{
    width: 0.8rem;
    height: 0.8rem;
    }
    span{
    padding: 0.1rem;
    font-size: 0.2rem;
    }
    }
    }
    </style>

发现好歌单

  1. src/views/HomeView.vue导入MusicList组件
  2. src/components/HomeView创建MusicList.vue组件
    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
    <template>
    <div class="musicList">
    <div class="musicList-top">
    <div class="title">发现好歌单</div>
    <div class="more">查看更多</div>
    </div>
    <div class="mlist">
    <div class="swiper-container" id="musicSwiper">
    <div class="swiper-wrapper">
    <!--循环获取封面、播放次数、歌单名-->
    <div class="swiper-slide" v-for="(item, i) in state.musicList" :key="i">
    <img :src="item.picUrl" :alt="item.name">
    <div class="count">
    <span>▷ {{changeValue(item.playCount) }}</span>
    </div>
    <div class="name">{{item.name}}</div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </template>

    <script>
    import 'swiper/css/swiper.css'
    import Swiper from 'swiper';
    import { getMusicList } from '@/api/index.js'
    import { reactive, onMounted, onUpdated } from 'vue'

    export default {
    name: 'MusicList',
    setup(){
    // 用来存放请求数据的空数组
    let state = reactive({ musicList: [] });
    // 过滤播放次数
    let changeValue = (num) =>{
    let res = 0;
    if(num >= 100000000){
    res = (num / 100000000).toFixed(0) + '亿';
    }else if(num > 10000){
    res = (num / 10000).toFixed(0) + '万';
    }
    return res;
    }
    // 请求'网友精选碟歌单'数据,并存进musicList数组内
    onMounted(async ()=>{
    let result = await getMusicList();
    // 查看result数据
    // console.log(result)
    state.musicList = result.data.result;
    })
    // 控制歌单滑动功能,歌单显示3个,总共有10个
    onUpdated(()=>{
    const swiper = new Swiper('#musicSwiper', {
    slidesPerView: 3,
    spaceBetween: 10,
    });
    })
    return {
    state, changeValue
    }
    }
    }
    </script>

    <style lang="less" scoped>
    .musicList{
    width: 7.5rem;
    padding: 0 0.4rem;
    // 发现好歌单与查看更多
    .musicList-top{
    display: flex;
    justify-content: space-between;
    height: 1rem;
    align-items: center;
    .title{
    font-size: 0.4rem;
    font-weight: 900;
    }
    .more{
    border: 1px solid #ccc;
    border-radius: 0.2rem;
    font-size: 0.24rem;
    height: 0.5rem;
    width: 1.2rem;
    text-align: center;
    line-height: 0.5rem;
    }
    }
    // 封面、播放次数、歌单名
    .mlist{
    .swiper-container{
    width: 100%;
    height: 3rem;
    .swiper-slide{
    display: flex;
    flex-direction: column;
    position: relative;
    // 封面
    img{
    width: 100%;
    height: auto;
    border-radius: 0.1rem;
    }
    // 播放次数
    .count{
    position: absolute;
    right: 0.1rem;
    top: 0.1rem;
    font-size: 0.24rem;
    color: white;
    display: flex;
    align-items: center;
    }
    // 歌单名
    .name{
    width: 100%;
    height: 0.9rem;
    padding: 0.1rem 0;
    overflow: hidden;
    font-size: 0.24rem;
    line-height: 0.4rem;
    }
    }
    }
    }
    }
    </style>
  3. src/api/index.js添加请求歌单数据
    1
    2
    3
    4
    5
    6
    ...

    // 获取推荐歌单,可选参数limit:取出数量,默认为10
    export let getMusicList = (limit=10) => {
    return axios.get(`${baseUrl}/personalized?limit=${limit}`)
    }
  4. 预览效果

歌单详情页面

设置路由跳转

  1. src/views创建ListView.vue,内容写个你好,用来测试跳转
    1
    2
    3
    <template>
    <h1>你好</h1>
    </template>
  2. src/router打开index.js,添加ListView.vue的路径
    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
    import { createRouter, createWebHistory } from 'vue-router'
    import HomeView from '../views/HomeView.vue'

    const routes = [
    {
    path: '/',
    name: 'home',
    component: HomeView
    },
    {
    path: '/listview',
    name: 'listview',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import('../views/ListView.vue')
    }
    ]

    const router = createRouter({
    history: createWebHistory(process.env.BASE_URL),
    routes
    })

    export default router
  3. src/components/HomeView/MusicList.vue修改div标签成router-link,并添加跳转路径,query是获取歌单id,根据id显示相应的内容
    1
    2
    3
    4
    5
    6
    7
    <!--循环获取封面、播放次数、歌单名-->
    <router-link :to="{ path:'/listview', query:{ id:item.id} }"
    class="swiper-slide"
    v-for="(item, i) in state.musicList" :key="i"
    >
    ...
    </router-link>
  4. src/App.vue添加a标签公共样式
    1
    2
    3
    4
    5
    6
    7
    <style lang="less">
    ...
    a{
    text-decoration: none;
    color: #3e4149;
    }
    </style>
  5. src/api/index.js添加请求歌单详情
    1
    2
    3
    4
    5
    ...
    // 获取歌单详情
    export let getPlaylistDetail = (id) => {
    return axios.get(`${baseUrl}/playlist/detail?id=${id}`)
    }
  6. src/views/ListView.vue添加歌单详情数据,顺便把ListViewTop组件注册进去
    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
    <template>
    <div class="listView">
    <!--歌单详情上部分,将数据传递给子组件-->
    <list-view-top :playlist="state.playlist"></list-view-top>
    </div>
    </template>

    <script>
    import ListViewTop from "@/components/ListView/ListViewTop.vue";
    import { getPlaylistDetail } from "@/api/index.js";
    import { onMounted, reactive } from 'vue'
    import { useRoute } from 'vue-router'

    export default {
    name: "ListView",
    components: {
    ListViewTop
    },
    setup(){
    // 获取当前路径
    const route = useRoute();
    // playlist用来存放歌单详情数据,creator用来存放作者数据
    // 不设置空对象,存放的数据在调用时会报undefined
    let state = reactive({
    playlist: {
    creator: {}
    }
    });
    onMounted(async () => {
    // 查看route,寻找到id后存到变量id里
    // console.log(route)
    let id = route.query.id;
    // 请求获取歌单详情数据
    let result = await getPlaylistDetail(id);
    // 查看歌单详情数据
    console.log(result)
    // 将得到的数据存进入
    state.playlist = result.data.playlist;
    })
    return { state };
    }
    };
    </script>

    <style scoped>

    </style>

歌单详情上部分

  1. src/components/ListView创建ListViewTop.vue组件

    创建ListView文件夹,用来管理歌单详情组件

    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
    <template>
    <div class="listViewTop">
    <div class="listViewTopNav">
    <!--背景图,动态获取图片路径-->
    <img class="bg" :src="playlist.coverImgUrl">
    <!--顶部左边,返回键与标题-->
    <div class="left">
    <!--点击三角图标返回到主页-->
    <svg class="icon" aria-hidden="true"
    @click="$router.go(-1)"
    >
    <use xlink:href="#icon-zuojiantou"></use>
    </svg>
    <div class="title">歌单</div>
    </div>
    <!--顶部右边,搜索与详情-->
    <div class="right">
    <svg class="icon search" aria-hidden="true">
    <use xlink:href="#icon-sousuo"></use>
    </svg>
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-gengduo-shuxiang"></use>
    </svg>
    </div>
    </div>
    <div class="content">
    <div class="contentLeft">
    <!--封面-->
    <img :src="playlist.coverImgUrl">
    <!--播放次数-->
    <span>▷ {{changeValue(playlist.playCount)}}</span>
    </div>
    <div class="contentRight">
    <!--歌单名-->
    <h3>{{playlist.name}}</h3>
    <!--作者头像与名字-->
    <div class="author">
    <img class="header" :src="playlist.creator.avatarUrl" alt="">
    <span>{{playlist.creator.nickname}}</span>
    </div>
    <!--描述内容-->
    <div class="description">{{playlist.description}}</div>
    </div>
    </div>
    <div class="iconList">
    <!--评论-->
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-pinglun"></use>
    </svg>
    <span>{{playlist.commentCount}}</span>
    </div>
    <!--分享-->
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-fenxiang-"></use>
    </svg>
    <span>{{playlist.commentCount}}</span>
    </div>
    <!--下载-->
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-xiazai"></use>
    </svg>
    <span>下载</span>
    </div>
    <!--多选-->
    <div class="iconItem">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-duoxuan-"></use>
    </svg>
    <span>多选</span>
    </div>
    </div>
    </div>
    </template>

    <script>
    export default {
    name: "ListViewTop",
    // 接收父组件提供的playlist数据
    props: ['playlist'],
    setup(){
    let changeValue = (num) =>{
    let res = 0;
    if(num >= 100000000){
    res = (num / 100000000).toFixed(0) + '亿';
    }else if(num > 10000){
    res = (num / 10000).toFixed(0) + '万';
    }
    return res;
    }
    return { changeValue }
    }
    };
    </script>

    <style scoped lang="less">
    .listViewTop{
    width: 7.5rem;
    padding: 0 0.4rem;
    .bg{
    position: fixed;
    left: 0;
    top: 0;
    width: 7.5rem;
    height: auto;
    // 设置-1减少层数使背景不会遮住内容
    z-index: -1;
    // 添加滤镜使背景模糊,降低对比度
    filter: blur(0.2rem) contrast(25%);
    }
    .listViewTopNav{
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 1.2rem;
    font-size: 0.4rem;
    padding-top: 0.3rem;
    .left, .right{
    display: flex;
    color: #fff;
    .icon{
    width: 0.5rem;
    height: 0.5rem;
    // SVG修改颜色
    fill: #fff;
    }
    .title{
    margin-left: 0.4rem;
    }
    .search{
    margin-right: 0.4rem;
    }
    }
    }
    .content{
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0;
    .contentLeft{
    position: relative;
    img{
    width: 2.8rem;
    height: 2.8rem;
    border-radius: 0.1rem;
    }
    span{
    position: absolute;
    right: 0.1rem;
    top: 0.1rem;
    font-size: 0.24rem;
    color: white;
    display: flex;
    align-items: center;
    }
    }
    .contentRight{
    width: 3.5rem;
    h3{
    margin-top: 0.05rem;
    color: white;
    overflow: hidden;
    text-overflow: ellipsis;
    // 下面3个必须写,可以实现2行文字
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    }
    .author{
    display: flex;
    align-items: center;
    margin: 0.25rem 0;
    .header{
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 0.3rem;
    margin-right: 0.2rem;
    }
    span{
    font-size: 0.28rem;
    font-weight: 500;
    color: rgb(204, 204, 204);
    }
    }
    .description{
    font-size: 0.24rem;
    color: rgba(204, 204, 204, 0.9);
    // color: rgba(0, 0, 0, 0.4);
    overflow: hidden;
    text-overflow: ellipsis;
    // 下面3个必须写,可以实现2行文字
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    }
    }
    }
    .iconList{
    display: flex;
    justify-content: space-around;
    .iconItem{
    display: flex;
    flex-direction: column;
    align-items: center;
    .icon{
    width: 0.6rem;
    height: 0.6rem;
    fill: rgba(255, 255, 255, 0.9);
    }
    span{
    font-size: 0.05rem;
    padding-top: 0.05rem;
    color: rgba(255, 255, 255, 0.9);
    }
    }
    }
    }
    </style>

歌单详情下部分

  1. ListView.vue导入playList组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <play-list :playlist="state.playlist"></play-list>

    import PlayList from "@/components/ListView/PlayList.vue";

    components: {
    ListViewTop,
    PlayList
    }

    setup(){
    ...
    // playlist用来存放歌单详情数据,creator用来存放作者数据,tracks存放歌曲
    // 不设置空对象,存放的数据在调用时会报undefined
    let state = reactive({
    playlist: {
    creator: {},
    tracks: []
    }
    });
    ...
    }
  2. src/components/ListView创建PlayList.vue组件
    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
    <template>
    <div class="playlist">
    <div class="playlist-top">
    <!--播放全部功能-->
    <div class="left">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-bofang"></use>
    </svg>
    <div class="text">
    <div class="title">播放全部</div>
    <div class="num">(共{{playlist.tracks.length}}首)</div>
    </div>
    </div>
    <!--收藏功能-->
    <div class="btn">+ 收藏({{changeValue(playlist.subscribedCount)}})</div>
    </div>
    <div class="list">
    <!--循环歌曲数据-->
    <div class="playItem"
    v-for="(item, i) in playlist.tracks"
    :key="i"
    >
    <div class="left">
    <!--索引-->
    <div class="index">{{i + 1}}</div>
    <div class="content">
    <!--歌曲名-->
    <div class="title">{{item.name}}</div>
    <!--作者名-->
    <div class="author">
    <span>{{(item.ar[0].name)}}</span>
    <span class="connect">-</span>
    <span>{{item.al.name}}</span>
    </div>
    </div>
    </div>
    <div class="right">
    <!--播放按钮-->
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-play"></use>
    </svg>
    <!--更多信息-->
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-gengduo-shuxiang"></use>
    </svg>
    </div>
    </div>
    </div>
    </div>
    </template>

    <script>
    export default {
    name: "PlayList",
    props: ['playlist'],
    setup(){
    let changeValue = (num) =>{
    let res = 0;
    if(num >= 100000000){
    res = (num / 100000000).toFixed(0) + '亿';
    }else if(num > 10000){
    res = (num / 10000).toFixed(0) + '万';
    }
    return res;
    }
    return { changeValue }
    }
    };
    </script>

    <style scoped lang="less">
    .playlist{
    width: 7.5rem;
    padding: 0 0.4rem;
    background-color: #fff;
    border-top-left-radius: 0.3rem;
    border-top-right-radius: 0.3rem;
    .playlist-top{
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 1.1rem;
    margin-top: 0.5rem;
    padding-top: 0.1rem;
    .left{
    display: flex;
    align-items: center;
    .icon{
    width: 0.5rem;
    height: 0.5rem;
    margin-right: 0.2rem;
    }
    .text{
    display: flex;
    align-items: center;
    .title{
    font-size: 0.35rem;
    }
    .num{
    font-size: 0.3rem;
    color: #ccc;
    }
    }
    }
    .btn{
    display: flex;
    align-items: center;
    height: 0.6rem;
    line-height: 0.6rem;
    padding: 0.4rem 0.3rem;
    font-size: 0.3rem;
    color: rgba(255, 255, 255, 0.9);
    background-color: #e84a5f;
    border-radius: 0.5rem;
    }
    }
    .list{
    .playItem{
    display: flex;
    justify-content: space-between;
    width: 100%;
    .left{
    display: flex;
    align-items: center;
    width: 4rem;
    height: 1rem;
    color: rgba(0, 0, 0, 0.5);
    .index{
    width: 0.2rem;
    font-size: 0.33rem;
    }
    .content{
    margin-left: 0.4rem;
    .title{
    font-size: 0.3rem;
    color: rgba(0, 0, 0, 0.9);
    // 防止标题溢出
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    }
    .author{
    font-size: 0.2rem;
    // 防止专辑名溢出
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    .connect{
    font-size: 0.01rem;
    }
    }
    }
    }
    .right{
    display: flex;
    align-items: center;
    //padding-top: 0.1rem;
    .icon{
    margin-top: 0.1rem;
    margin-right: 0.3rem;
    width: 0.5rem;
    height: 0.5rem;
    }
    }
    }
    }
    }
    </style>
  3. src/store/index.js增加全局属性方法
    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
    import { createStore } from 'vuex'

    export default createStore({
    state: {
    // 初始化避免报错
    playlist: [{
    name: '岁月如歌',
    id: 26608787,
    al: {
    id: 2532179,
    name: "2013 陈奕迅 music life 精选",
    pic: 109951166656425500,
    picUrl: "http://p3.music.126.net/1I8ELtF6pswNRAVs4CwjfA==/109951166656425505.jpg",
    pic_str: "109951166656425505",
    },
    ar: [{
    name: '陈奕迅'
    }]
    }],
    playCurrentIndex: 0
    },
    mutations: {
    setPlaylist(state, value){
    state.playlist = value;
    },
    },
    getters: {
    },
    actions: {
    },
    modules: {
    }
    })

底部全局播放控件

音乐播放与暂停

  1. src/App.vue导入PlayController.vue组件
    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
    <template>
    <router-view/>
    <!--底部全局播放控件-->
    <play-controller></play-controller>
    </template>

    <script>
    import PlayController from "@/components/ListView/PlayController.vue";

    export default {
    name: 'App.vue',
    components: {
    PlayController
    }
    }
    </script>

    <style lang="less">
    *{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: sans-serif;
    }
    .icon {
    width: 1rem;
    height: 1rem;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
    }
    a{
    text-decoration: none;
    color: #3e4149;
    }
    </style>
  2. src/components/ListView创建PlayController.vue
    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
    <template>
    <div class="playController">
    <div class="left">
    <!--封面-->
    <img :src="playlist[playCurrentIndex].al.picUrl" alt="">
    <div class="content">
    <!--歌曲名-->
    <div class="title">{{playlist[playCurrentIndex].name}}</div>
    <div class="tips">横滑可以切换上下首</div>
    </div>
    </div>
    <div class="right">
    <!--播放按钮-->
    <svg v-if="paused"
    class="icon"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-16"></use>
    </svg>
    <!--暂停按钮-->
    <svg v-else
    class="icon pause"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-zanting"></use>
    </svg>
    <!--列表按钮-->
    <svg class="icon playlistMusic" aria-hidden="true">
    <use xlink:href="#icon-bofangliebiao"></use>
    </svg>
    </div>
    <audio ref="audio"
    :src="`https://music.163.com/song/media/outer/url?id=${playlist[playCurrentIndex].id}.mp3`"
    >
    </audio>
    </div>
    </template>

    <script>
    import { mapState } from 'vuex'

    export default {
    name: "PlayController",
    data(){
    return{
    paused: true
    }
    },
    computed: {
    ...mapState(['playlist', 'playCurrentIndex'])
    },
    methods: {
    play(){
    // 点击按钮后判断播放按钮是否开启
    if(this.$refs.audio.paused){
    // 点击播放
    this.$refs.audio.play();
    // 隐藏播放按钮
    this.paused = false;
    }else{
    // 点击暂停
    this.$refs.audio.pause();
    // 隐藏暂停按钮
    this.paused = true;
    }
    }
    }
    };
    </script>

    <style scoped lang="less">
    .playController{
    width: 7.5rem;
    height: 1.2rem;
    left: 0;
    bottom: 0;
    background-color: #fff;
    position: fixed;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-top: 1px solid #ccc;
    .left{
    display: flex;
    padding: 0 0.4rem;
    img{
    width: 0.8rem;
    height: 0.8rem;
    margin-right: 0.2rem;
    border-radius: 0.5rem;
    }
    .content{
    .title{
    font-size: 0.3rem;
    color: rgba(0, 0, 0, 0.8);
    // 防止标题溢出
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    }
    .tips{
    margin-top: 0.05rem;
    font-size: 0.1rem;
    color: rgba(0, 0, 0, 0.5);
    }
    }
    }
    .right{
    display: flex;
    align-items: center;
    .icon{
    width: 0.6rem;
    height: 0.6rem;
    margin: 0 0.2rem;
    }
    .pause{
    width: 0.57rem;
    height: 0.57rem;
    padding-right: 0.05rem;
    }
    .playlistMusic{
    width: 0.57rem;
    height: 0.57rem;
    padding-right: 0.05rem;
    //svg线条变细
    shape-rendering: crispEdges;
    }
    }
    }
    </style>

切换歌曲

  1. api/index.js添加修改播放索引
    1
    2
    3
    4
    5
    6
    7
    8
    9
    mutations: {
    setPlaylist(state, value){
    state.playlist = value;
    },
    // 修改播放索引
    setPlayIndex(state, value){
    state.playCurrentIndex = value;
    }
    },
  2. PlayList.vue添加setPlayIndex点击事件,识别播放第几首
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div class="left" @click="setPlayIndex(i)">
    ...
    </div>

    <!--播放按钮-->
    <svg class="icon" aria-hidden="true"
    @click="setPlayIndex(i)"
    >
    <use xlink:href="#icon-play"></use>
    </svg>
  3. 预览效果

播放音乐页面

  1. 老师讲得非常乱,bug很多,部分地方我也懵,不细致写了,直接扔完整代码
  2. PlayController.vue导入PlayMusic.vue组件
    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
    <template>
    <div class="playController">
    <!--点击封面歌曲名显示播放界面-->
    <div class="left" @click="show=!show">
    <!--封面-->
    <img :src="playlist[playCurrentIndex].al.picUrl" alt="">
    <div class="content">
    <!--歌曲名-->
    <div class="title">{{playlist[playCurrentIndex].name}}</div>
    <div class="tips">横滑可以切换上下首</div>
    </div>
    </div>
    <div class="right">
    <!--播放按钮-->
    <svg v-if="paused"
    class="icon"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-16"></use>
    </svg>
    <!--暂停按钮-->
    <svg v-else
    class="icon pause"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-zanting"></use>
    </svg>
    <!--列表按钮-->
    <svg class="icon playlistMusic" aria-hidden="true">
    <use xlink:href="#icon-bofangliebiao"></use>
    </svg>
    </div>
    <audio ref="audio"
    :src="`https://music.163.com/song/media/outer/url?id=${playlist[playCurrentIndex].id}.mp3`"
    >
    </audio>
    </div>
    <!--播放界面,@back接收到子组件请求时进行取反,show控制是否显示,传递数据给子组件-->
    <play-music
    @back="show=!show"
    v-show="show"
    :paused="paused"
    :play="play"
    :playDetail="playlist[playCurrentIndex]"
    >
    </play-music>
    </template>

    <script>
    import { mapState } from 'vuex'
    import PlayMusic from "@/components/ListView/PlayMusic.vue";

    export default {
    name: "PlayController",
    components: {
    PlayMusic
    },
    data(){
    return{
    paused: true,
    show: false,
    }
    },
    computed: {
    ...mapState(['playlist', 'playCurrentIndex'])
    },
    methods: {
    play(){
    // 点击按钮后判断播放按钮是否开启
    if(this.$refs.audio.paused){
    // 点击播放
    this.$refs.audio.play();
    // 隐藏播放按钮
    this.paused = false;
    this.UpdateTime()
    }else{
    // 点击暂停
    this.$refs.audio.pause();
    // 隐藏暂停按钮
    this.paused = true;
    clearInterval(this.$store.state.id)
    }
    },
    // 歌词更新时间
    UpdateTime(){
    this.$store.state.id = setInterval(() => {
    this.$store.commit('setCurrentTime', this.$refs.audio.currentTime)
    },1000)
    }
    },
    mounted(){
    // 调用store下的异步函数,第一个参数函数名reqLyric,第二个参数传入id
    this.$store.dispatch(
    'reqLyric',
    { id: this.playlist[this.playCurrentIndex].id }
    )

    },
    updated() {
    this.$store.dispatch(
    'reqLyric',
    { id: this.playlist[this.playCurrentIndex].id }
    )
    }
    };
    </script>

    <style scoped lang="less">
    .playController{
    width: 7.5rem;
    height: 1.2rem;
    left: 0;
    bottom: 0;
    background-color: #fff;
    position: fixed;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-top: 1px solid #ccc;
    .left{
    display: flex;
    padding: 0 0.4rem;
    img{
    width: 0.8rem;
    height: 0.8rem;
    margin-right: 0.2rem;
    border-radius: 0.5rem;
    }
    .content{
    .title{
    font-size: 0.3rem;
    color: rgba(0, 0, 0, 0.8);
    // 防止标题溢出
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    }
    .tips{
    margin-top: 0.05rem;
    font-size: 0.1rem;
    color: rgba(0, 0, 0, 0.5);
    }
    }
    }
    .right{
    display: flex;
    align-items: center;
    .icon{
    width: 0.6rem;
    height: 0.6rem;
    margin: 0 0.2rem;
    }
    .pause{
    width: 0.57rem;
    height: 0.57rem;
    padding-right: 0.05rem;
    }
    .playlistMusic{
    width: 0.57rem;
    height: 0.57rem;
    padding-right: 0.05rem;
    //svg线条变细
    shape-rendering: crispEdges;
    }
    }
    }
    </style>
  3. src/components/ListView创建PlayMusic.vue组件
    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
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    <template>
    <div class="playMusic">
    <!--动态获取背景占满屏幕-->
    <div class="bg"
    :style="{ backgroundImage: `url(${playDetail.al.picUrl})`}"
    >
    </div>
    <div class="playTop">
    <!--返回键,点击子组件向父组件发送back请求-->
    <div class="back"
    @click="$emit('back'); goCover()"
    >
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-zuojiantou"></use>
    </svg>
    </div>
    <!--歌曲名和作者-->
    <div class="center">
    <div class="title">
    <!--marquee滚动标签-->
    <marquee behavior="scroll" direction="left">
    {{playDetail.name}}
    </marquee>
    </div>
    <div class="author">{{playDetail.ar[0].name}}</div>
    </div>
    <!--分享-->
    <div class="share">
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-fenxiang-"></use>
    </svg>
    </div>
    </div>
    <div class="playContent"
    v-show="!isLyric"
    @click="goLyrics()"
    >
    <!--指针,点击播放获取active样式-->
    <img :class="{active: !paused}"
    class="needle"
    src="@/assets/img/needle-ab.png" alt=""
    >
    <!--磁碟-->
    <img class="disc" src="@/assets/img/disc-ip6.png" alt="">
    <!--磁碟中间的背景图,点击播放获取rotate样式-->
    <img :class="{rotate: !paused}"
    class="playImg"
    :src="playDetail.al.picUrl" alt=""
    >
    </div>
    <!--歌词-->
    <div class="playLyric"
    v-show="isLyric"
    @click="goCover()"
    >
    <p :class="{active: (
    (currentTime * 1000) > item.pre &&
    currentTime * 1000 < item.time)}"
    v-for="(item, i) in $store.getters.lyricList"
    :key="i"
    ref="playLyric"
    >
    {{item.lyric}}
    </p>
    </div>
    <div class="progress"></div>
    <div class="playFooter">
    <!--循环-->
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-xunhuan"></use>
    </svg>
    <!--上一首-->
    <svg class="icon" aria-hidden="true"
    @click="goPlay(-1)"
    >
    <use xlink:href="#icon-shangyishoushangyige"></use>
    </svg>
    <!--播放-->
    <svg v-if="paused"
    class="icon play"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-bofang"></use>
    </svg>
    <!--暂停-->
    <svg v-else
    class="icon play"
    aria-hidden="true"
    @click="play"
    >
    <use xlink:href="#icon-zanting"></use>
    </svg>
    <!--下一首-->
    <svg class="icon" aria-hidden="true"
    @click="goPlay(1)"
    >
    <use xlink:href="#icon-xiayigexiayishou"></use>
    </svg>
    <!--待播列表-->
    <svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-bofangliebiao"></use>
    </svg>
    </div>
    </div>
    </template>

    <script>
    import { mapState } from "vuex";

    export default {
    name: "PlayMusic",
    props: ['playDetail', 'paused', 'play'],
    data(){
    return{
    isLyric: false
    }
    },
    computed:{
    ...mapState(['lyric', 'currentTime', 'playlist', 'playCurrentIndex']),
    },
    watch:{
    currentTime(newValue){
    let p = document.querySelector('p.active');
    if(p){
    let offsetTop = p.offsetTop;
    this.$refs.playLyric.scrollTop = p.offsetTop;
    }
    }
    },
    methods:{
    // 切换歌曲
    goPlay(num){
    let index = this.playCurrentIndex + num;
    if(index < 0){
    index = this.playlist.length - 1;
    }else if(index === this.playlist.length){
    index = 0;
    }
    this.$store.commit('setPlayIndex', index)
    },
    goCover(){
    this.isLyric = false;
    },
    goLyrics(){
    this.isLyric = !this.isLyric;
    }
    }
    };
    </script>

    <style scoped lang="less">
    .playMusic{
    position: fixed;
    left: 0;
    top: 0;
    //宽高各100vw、100vh占满全屏
    width: 100vw;
    height: 100vh;
    //优先级设高一点,避免主页打开显示其他内容
    z-index: 1;
    //添加滤镜后四周透明需要再加白色背景遮挡
    background-color: #fff;
    .bg{
    position: absolute;
    left: 0;
    top: 0;
    width: 100vw;
    height: 100vh;
    background-size: auto 100%;
    background-position: center;
    // 添加滤镜使背景模糊,降低亮度
    filter: blur(0.8rem) brightness(80%);
    }
    .playTop{
    display: flex;
    position: absolute;
    justify-content: space-between;
    align-items: center;
    left: 0;
    top: 0;
    width: 7.5rem;
    height: 1.2rem;
    padding: 0.8rem 0.4rem;
    color: #fff;
    .icon{
    width: 0.7rem;
    height: 0.7rem;
    fill: #fff;
    }
    .center{
    width: 4.8rem;
    height: 0.75rem;
    line-height: 0.35rem;
    .title{
    font-size: 0.35rem;
    flex: 1;
    }
    .author{
    font-size: 0.1rem;
    color: #ccc;
    }
    }
    }
    .playContent{
    position: absolute;
    width: 7.5rem;
    height: 7.5rem;
    left: 0;
    top: 1.5rem;
    .needle{
    position: absolute;
    width: 2.2rem;
    height: auto;
    left: 3.5rem;
    //避免指针被磁碟挡住
    z-index: 1;
    transform-origin: 0.3rem 0;
    transform: rotate(-20deg);
    transition: all 1s;
    }
    //指针移动到磁碟上
    .needle.active{
    position: absolute;
    width: 2.2rem;
    height: auto;
    left: 3.5rem;
    //避免指针被磁碟挡住
    z-index: 1;
    transform-origin: 0.3rem 0;
    transform: rotate(2deg);
    transition: all 1s;
    }
    .disc{
    position: absolute;
    width: 6rem;
    height: auto;
    left: calc(50% - 3rem);
    top: 2.2rem;
    }
    .playImg{
    position: absolute;
    width: 4rem;
    height: 4rem;
    border-radius: 2rem;
    left: calc(50% - 2rem);
    top: 3.2rem;
    }
    .playImg{
    position: absolute;
    width: 4rem;
    height: 4rem;
    border-radius: 2rem;
    left: calc(50% - 2rem);
    top: 3.2rem;
    }
    @keyframes changeDeg{
    0%{
    transform: rotate(0deg);
    }
    100%{
    transform: rotate(360deg);
    }
    }
    //磁碟中间背景图旋转动画
    .playImg.rotate{
    position: absolute;
    width: 4rem;
    height: 4rem;
    border-radius: 2rem;
    left: calc(50% - 2rem);
    top: 3.2rem;
    animation: changeDeg 15s linear 1s infinite;
    }
    }
    .playFooter{
    position: absolute;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-left: 0.6rem;
    padding-right: 0.6rem;
    padding-bottom: 0.8rem;
    width: 7.5rem;
    height: 1.5rem;
    left: 0;
    bottom: 0;
    .icon{
    fill: #fff;
    width: 0.6rem;
    height: 0.6rem;
    }
    .play{
    width: 1rem;
    height: 1rem;
    }
    }
    .playLyric{
    position: absolute;
    width: 7.5rem;
    height: 8rem;
    left: 0;
    top: calc(50% - 4rem);
    text-align: center;
    padding: 0.2rem 0;
    color: rgba(255, 255, 255, 0.8);
    //歌词溢出滚动
    overflow: scroll;
    //歌词选中时变色
    .active{
    color: #a5dee5;
    }
    }
    }
    </style>
  4. src/api/index.js添加获取歌词的请求
    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
    import axios from 'axios';

    let baseUrl = 'http://localhost:3000'

    // 获取轮播图的api,type:资源类型,对应以下类型,默认为0即PC
    // 1: android; 2: iphone; 3: ipad
    export let getBanner = (type=0) => {
    return axios.get(`${baseUrl}/banner?type=${type}`)
    }

    // 获取推荐歌单,可选参数limit:取出数量,默认为10
    export let getMusicList = (limit=10) => {
    return axios.get(`${baseUrl}/personalized?limit=${limit}`)
    }

    // 获取歌单详情
    export let getPlaylistDetail = (id) => {
    return axios.get(`${baseUrl}/playlist/detail?id=${id}`)
    }

    // 获取歌词
    export let getLyric = (id) => {
    return axios.get(`${baseUrl}/lyric?id=${id}`)
    }

    export default {
    getBanner, getMusicList, getPlaylistDetail, getLyric
    }
  5. src/store/index.js添加播放音乐页面属性方法
    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
    import { createStore } from 'vuex'
    import api from "@/api/index.js"

    export default createStore({
    state: {
    // 初始化避免报错
    playlist: [{
    name: '岁月如歌',
    id: 26608787,
    al: {
    id: 2532179,
    name: "2013 陈奕迅 music life 精选",
    pic: 109951166656425500,
    picUrl: "http://p3.music.126.net/1I8ELtF6pswNRAVs4CwjfA==/109951166656425505.jpg",
    pic_str: "109951166656425505",
    },
    ar: [{
    name: '陈奕迅'
    }]
    }],
    playCurrentIndex: 0,
    // 歌词
    lyric: '',
    // 歌词更新时间
    currentTime: 0,
    },
    mutations: {
    // 修改播放列表
    setPlaylist(state, value){
    state.playlist = value;
    },
    // 修改播放索引
    setPlayIndex(state, value){
    state.playCurrentIndex = value;
    },
    // 修改歌词
    setLyric(state, value){
    state.lyric = value;
    },
    // 修改歌词更新时间
    setCurrentTime(state, value){
    state.currentTime = value;
    }
    },
    actions: {
    // 异步获取歌词后,调用修改歌词方法
    async reqLyric(content, payload){
    let result = await api.getLyric(payload.id)
    content.commit('setLyric', result.data.lrc.lyric)
    }
    },
    getters: {
    // 分割歌词获取分、秒、毫、词、总时间
    lyricList(state){
    let arr = state.lyric.split(/\n/igs).map((item, i, arr) => {
    let min = parseInt(item.slice(1, 3));
    let sec = parseInt(item.slice(4, 6));
    let milli = parseInt(item.slice(7, 10));
    return {
    min, sec, milli,
    lyric:item.slice(11, item.length),
    content: item,
    time: milli +
    sec * 1000 +
    min * 60 * 1000
    }
    })
    arr.forEach((item, i) => {
    if (i===0){
    item.pre = 0
    }else{
    item.pre = arr[i - 1].time
    }
    })
    return arr
    }
    },
    modules: {
    }
    })
  6. 预览效果

总结

  1. vue全家桶需要多练习,想要项目很好看,css美化也很重要