音乐播放器项目记录

《歌手详情页的开发---vuex的使用》

Posted by Charlene on November 1, 2019

1. 歌手详情页子路由配置

singer-detail组建的开发:创建并编写简单的页面

在index.js中重新配置路由,点击歌手就可以转到详情列表了

{
  path: '/singer',
  component: Singer,
  children: [
    {
      path: ':id',
      component: SingerDetail
    }
  ]
}

配置好子路由之后,要有一个routerview去承载这个子路由,我们回到singer组件,在list-view(歌手列表组件)的下边挂载子router-view

<template>
  <div class="singer">
    <!-- 将singers数据命名为data变量传递给listview.vue组件 -->
    <list-view @select="selectSinger" :data="singers"></list-view>
    <!-- 承载子路由SingerDetail -->
    <router-view></router-view>
  </div>
</template>

添加路由跳转逻辑,我们希望我们点击歌手列表的时候,可以进行路由跳转,歌手列表基于list-view实现,所以在listview组件中,添加一个点击事件selectItem();

<ul>
  <!-- 这里面的每一个li代表每个组别里面的每一位歌手 -->
  <li @click="selectItem(item)" v-for="(item, index) in group.items" :key="index" class="list-group-item">
    <img class="avatar" v-lazy="item.avatar">
    <span class="name"></span>
  </li>
</ul>

在listview中添加点击事件并将item派发出去,点击事件没有业务逻辑,仅仅是将数据传出去

selectItem(item) {
  this.$emit('select', item) // 将点击的是哪一位歌手派发出去
},

回到singer.vue界面监听函数

<!-- 将singers数据命名为data变量传递给listview.vue组件 -->
<list-view @select="selectSinger" :data="singers"></list-view>

methods中实现监听函数

// 点击某个歌手
selectSinger(singer) {
  this.$router.push({
    path: `/singer/${singer.id}` // router的编程式跳转接口
  })
  console.log(`${singer.id}`)
},

点击歌手出现歌手详情,但是界面的跳转略显生硬,我们为singer-detail的跳转增加一个transition动画

<template>
  <transition name="slide">
      <music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list>
  </transition>
</template>
.slide-enter-active, .slide-leave-active
    transition all 0.3s
.slide-enter, .slide-leave-to
    transform translate3d(100%, 0, 0)

2. Vuex 初始化及歌手数据的配置

当我们点击歌手列表切换到歌手详情页的时候,需要把歌手的数据,歌手的名称,图片等,通过参数的传递也是可以解决的,利用vuex解决这个问题也可。

vuex官方文档
vuex工作流程图 State:所有组件的所有状态和数据,放在同一的内存空间来管理。
Getter:Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Mutation:你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。
Action:Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。

使用场景:解决多个组件之间的状态共享,这些组件可能是一些关联度很低的组件,所以我们想要共享数据就比较困难。比如遇到一些路由跳转场景,如果我们传递的参数很复杂的话,vuex是很好的选择

index: vuex的入口函数src->store->index.js
state: 状态管理:src->store->state.js
mutations: 建立mutations.js,Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
mutation-types: 使用常量替代 Mutation 事件类型
actions:异步修改,或者对mutation做一些封装
getters: 对state数据进行映射

首先,定义state.js

const state = {
    singer: {},
    playing: false // 播放状态
}
 
export default state

mutation-type:定义常量

// 定义mutation的常量
export const SET_SINGER = 'SET_SINGER'

然后在mutations.js中定义修改的操作:

import * as types from './mutation-types'
 
const mutations = { // mutation相关的修改方法
    [types.SET_SINGER](state, singer) { // 当前状态树的state,提交mutation时传的payload
        state.singer = singer
    }
}

有了mutation修改数据,我们怎么映射到state中去呢,通常从getters中取state数据到vue components中

export const singer = state => state.singer // 使用getter取到组件里的数据

action.js暂时不用初始化,因为还没有相关action的修改 接下来将入口js,即index.js进行初始化

import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
import createLogger from 'vuex/dist/logger'
 
Vue.use(Vuex) // 注册插件
 
// 调试工具
const debug = process.env.NODE_ENV !== 'production'
 
export default new Vuex.Store({ // 我们要去export store的一个实例,单例模式
    actions,
    getters,
    state,
    mutations,
    strict: debug, // 检测state的修改是不是来源于mutation
    plugins: debug ? [createLogger()] : [] // 通过mutation修改state的时候会在控制台打印logger
})

在main.js中修改,将store注入到vue中,此时,vuex的初始化配置完成

import store from './store'
new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

回到singer.vue界面:当singer.vue组件进行跳转的时候,我没去去set这个singer,我们把这个singer当成vuex的一个数据

import {mapMutations} from 'vuex'

mapMutation是对mutation进行了一层封装,我们import Mapmutation之后,我们只要在methods函数的最后,通过扩展运算符的方式调用mapMutations,去做一个对象的映射,将mutation的修改映射成一个方法名,setSinger,他实际上就是mutation-types中的SET_SINGER常量

...mapMutations({
   setSinger: 'SET_SINGER'
 })

之后再代码中,我们就可以调用setSinger方法往state中写数据了

// 点击某个歌手
selectSinger(singer) {
  this.$router.push({
    path: `/singer/${singer.id}` // router的编程式跳转接口
  })
  // 在setSinger中将数据(singer)传进来
  // 实现了对一个mutation的提交,修改了state,实际上执行了mutations.js中的types.SET_SINGER函数
  // state.singer = singer,实现了对state的修改,这里是往state中写了singer数据
  this.setSinger(singer)
},

将singer数据提交到state之后,然后在详情界面singer-detail.vue中去获取数据

import {mapGetters} from 'vuex' // vuex为取出数据提供的语法糖

在computed中执行mapGetters函数

computed: { // getter映射的就是computed
    ...mapGetters([
// export const singer = state => state.singer // 使用getter取到组件里的数据
      'singer' // 对应到store中的getters定义的singer
    ])
}

做了这一层映射以后,我们就相当于在vue实例中挂载了一个叫singer的属性,然后我们就可以拿到singer,在created的时候进行consloe.log()进行测试

created() {
   console.log(this.singer) // 测试输出singer数据
}

测试输出singer数据

3. 歌手详情数据抓取

歌手详情接口来源
singermid不同,歌手详情数据就不同,其他的都一样

// src->api>singer.js
// 歌手详情数据抓取
export function getSingerDetail(singerId) {
  const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg'

  const data = Object.assign({}, commonParams, {
    loginUin: 0,
    hostUin: 0,
    // format: 'jsonp',
    inCharset: 'utf8',
    notice: 0,
    platform: 'yqq',
    needNewCode: 0,
    singermid: singerId,
    order: 'listen',
    begin: 0,
    num: 100,
    songstatus: 1
  })

  return jsonp(url, data, options)
}

回到src->common->singer-detail.vue中,在created中定义获取数据的方法

import {getSingerDetail} from 'api/singer'
import {ERR_OK} from 'api/config'
    created() {
      //  console.log(this.singer) // 测试输出singer数据
      // 调用方法获取数据
      this._getDetail()
    }

在_getDetail()中调用刚才的api方法getSingerDetail,之前我们已经通过…mapGetter()获取到了singer,这里讲singer的id传入getSingerDetail函数;找不到歌手id的时候,刷新会退回到singer路由

_getDetail() {
  // 用户在歌手详情页面上刷新  则回退到上一页面
  if (!this.singer.id) {
    this.$router.push('/singer')
    return
  }
  // 因为我们已经获取到了singer数据,此处可以通过this.singer直接调用获取歌手的id
  getSingerDetail(this.singer.id).then((res) => {
    if (res.code === ERR_OK) {
      // console.log(res.data.list)
      this.songs = this._normalizeSongs(res.data.list)
      // console.log(this.songs)
    }
  })
},

console.log(res.data.list)

4. 歌手详情数据处理和Song类的封装

_normalizeSongs函数是对歌手数据进行格式化处理:跟之前的singer类中进行new singer处理相同,在这里,我们创建一个song类,对song的相关属性进行封装。参数很多的场景就可以设计成类,没有设计成对象,是因为类的扩展性要比对象好很多。
创建common->js->song.js,类似于singer的id,name,avatar

// 我们需要在 _getSingerList 获取到的数据里面去提取我们需要的部分,来构造成我们需要的数据对象
export default class Song {
  constructor({id, mid, singer, name, album, duration, image, url}) {
    this.id = id
    this.mid = mid
    this.name = name
    this.album = album
    this.duration = duration
    this.image = image
    this.url = url
    this.singer = singer
  }

在singerDetail里面我们去维护一个data,return一个song

data() {
  return {
    songs: []
  }
},

singer.js 获取vkey

export function getVkey(songmid) {
  const url = 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg'
  const data = Object.assign({}, {
    loginUin: 0,
    hostUin: 0,
    platform: 'yqq',
    uin: 0,
    needNewCode: 0,
    cid: 205361747,
    songmid: songmid,
    filename: `C400${songmid}.m4a`,
    guid: 5544337966
  })
  return jsonp(url, data)
}

createSong(musicData, vkey)) // 将获取到的musicData源数据转化为我们定义好的song类,直接new的方式代码量较多,编写一个工厂方法代替,所以在js->song.js中

export function createSong(musicData, vkey) {
  return new Song({
    id: musicData.songid,
    mid: musicData.songmid,
    singer: filterSinger(musicData.singer), // 处理一首歌有两个歌手的情况
    name: musicData.songname,
    album: musicData.albumname,
    duration: musicData.interval, // 歌曲的时长
    image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${musicData.albummid}.jpg?max_age=2592000`,
    url: `http://aqqmusic.tc.qq.com/amobile.music.tc.qq.com/C400${musicData.songmid}.m4a?vkey=${vkey}&guid=5544337966&uin=0&fromtag=38`
  })
}

singer展示的是一个歌手,如果有两个的话就用/来区分两个及以上的歌手,在元数据中singer是一个数组,这不是我们想要的格式,我们希望数据可以直接应用到DOM上,所以对singer数据进行过滤

/**
 * 要将数组中的歌手名  拼接成一个字符串  薛之谦/欧阳娜娜
 * @param {array} singer 歌手的情况{id:xx,name: xx }
 */
function filterSinger(singer) {
  let ret = []
  // 如果传入的歌手为空
  if (!singer) {
    return ''
  }
  singer.forEach((s) => {
    ret.push(s.name)
  })
  return ret.join('/')
}

回到singer-detail.vue引入createSong,在_normalizeSongs中对歌手详情list数据做一个规范化处理

_normalizeSongs(list) {
  let ret = []
  list.forEach((item) => {
    let {musicData} = item // 对象解构赋值,只取musicData数据
    getVkey(musicData.songmid).then((res) => {
      const vkey = res.data.items[0].vkey
      if (musicData.songid && musicData.albumid) {
        ret.push(createSong(musicData, vkey))
      }
    })
  })
  return ret
}

这样,ret数组中所有的Song对象,都是我们对源数据进行处理后提取的我们需要的数据格式了,之后将_normalizeSongs函数应用到getSingerDetail函数中,输出之后,数据就是我们想要的格式了

_getDetail() {
  // 用户在歌手详情页面上刷新  则回退到上一页面
  if (!this.singer.id) {
    this.$router.push('/singer')
    return
  }
  // 因为我们已经获取到了singer数据,此处可以通过this.singer直接调用获取歌手的id
  getSingerDetail(this.singer.id).then((res) => {
    if (res.code === ERR_OK) {
      // console.log(res.data.list)
      this.songs = this._normalizeSongs(res.data.list)
      console.log(this.songs)
    }
  })
},

console.log(this.songs)

created() {
    //  console.log(this.singer) // 测试输出singer数据
    // 调用方法获取数据
    this._getDetail()
}

5. music-list 组件开发

歌手的详情页的歌单部分和热门歌单推荐详情页歌单部分是类似的,所以我们将其抽象成一个musiclist组件,通过props传入不同的数据来实现不同的歌单列表
获取完数据并进行规范化处理之后,编写music-list.vue组件,写好主体框架,

<template>
  <div class="music-list">
    <div class="back" @click="back">
      <i class="icon-back"></i>
    </div>
    <h1 class="title" v-html="title"></h1>
    <div class="bg-image" :style="bgStyle" ref="bgImage">
      <div class="filter" ref="filter"></div>
    </div>
  </div>
</template>

music-list.vue组件在props中接受父组件传递过来的数据:

props: {
  bgImage: {
    type: String,
    default: ''
  },
  songs: {
    type: Array,
    default: () => []
  },
  title: {
    type: String,
    default: ''
  }
},

将其引入到父组件singer-detail.vue中,singer-detail.vue没有其他dom结构了,只需要调用music-list组件就成,父组件singer-detail主要负责给子组件music-list传递数据即可,不仅要忘了引用注册组件

import MusicList from 'components/music-list/music-list'
components: {
  MusicList
}
<template>
  <transition name="slide">
    <!-- <div class="singer-detail"></div> -->
    <music-list :bg-image="bgImage" :title="title" :songs="songs"></music-list>
  </transition>
</template>

在singer-detail.vue中,songs直接将当当前的songs传入即可,通过计算属性拿到title和bgImage的值,singer在mapGetter中得到

data() {
  return {
    songs: []
  }
},
computed: {
  title() {
    return this.singer.name
  },
  bgImage() {
    return this.singer.avatar
  },
  // 把 `this.singer` 映射为 `this.$store.getters.singer`
  ...mapGetters([
    'singer'
  ])
},

回到list-music.vue处理拿到的数据,填充DOM 显示歌手名:

<h1 class="title" v-html="title"></h1>

显示歌手图片:

<div class="bg-image" :style="bgStyle">

bgStyle可以通过一个计算属性得到

computed: {
  bgStyle() {
    return `background-image:url(${this.bgImage})`
  }
},

这样我们就将图片加载到了组件的上方,,图片下边的歌单样式都是复用的,可以复用,抽象成song-list组件
为了方便复用歌曲列表,将列表抽象成一个基础组件src->base->song-list->song-list.vue
首先在props中定义要接受的数据

props: { // 需要接收的数据
  songs: {
    type: Array,
    default: () => []
  }
},

拿到基础数据之后去填充DOM,基础框架如下

<template>
  <div class="song-list">
    <ul>
      <li @click="selectItem(song, index)" v-for="(song, index) in songs" :key="song.id" class="item">
        <div class="content">
          <h2 class="name"></h2>
          <p class="desc"></p>
        </div>
      </li>
    </ul>
  </div>
</template>

然后去获得歌曲的描述: getDesc()

methods: {
  getDesc(song) {
    return `${song.singer}---${song.album}`
  }
}

歌曲列表组件song-list.vue开发完成,我们回到music-list(song.vue+背景)中去应用这个组件。
music-list.vue中引入到子组件song-list.vue,并引入scroll.vue。为控制内部样式,在song-list.vue的外边添加一个wrapper,即song-list-wrapper。将songs作为data传入scroll中,在scroll组件中规定只有data中含有数据时才会滚动,并将songs的值传递给song-list组件
music-list.vue中引入子组件:

import Scroll from 'base/scroll/scroll'
import SongList from 'base/song-list/song-list'

music-list.vue中注册子组件:

components: {
  Scroll,
  SongList
}
<scroll @scroll="scroll" :probe-type="probeType" :listen-scroll="listenScroll" :data="songs" class="list" ref="list">
  <div class="song-list-wrapper">
    <song-list @select="selectItem" :songs="songs"></song-list>
  </div>
  <div class="loading-container" v-show="!songs.length">
    <loading></loading>
  </div>
</scroll>

歌手图片消失 之后,指定歌手的歌曲列表实现并可以滚动,但是高度不对,歌手的图片消失了,所以在music-list.vue组件中,为scroll添加一个ref,拿到sroll的DOM,为其添加一个top值,为图片腾出空间(因为不同的设备背景图的高度是不一样的)

<div class="bg-image" :style="bgStyle" ref="bgImage">
  <scroll :data="songs" class="list" ref="list">

我们在mounted的生命周期下控制它的top值

mounted() {
  this.imageHeight = this.$refs.bgImage.clientHeight
  this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT
  // list是一个component对象,要通过$el取得它的DOM,将scoll的高度设置为背景图的高度
  this.$refs.list.$el.style.top = `${this.imageHeight}px`
},

我们希望列表向上滑动的时候,上部图片可以向上移动
1)先将music-list的overflow属性去掉,这样歌曲列表(song-list.vue)就可以滚动上去盖住图像部分了

.list
  overflow hidden // 去掉

2)这时需要一个位于歌单列表下方的图层(bg-layer),当歌单列表向上滑动的时候filter也跟着向上滑动,盖住下方的歌手图片

<!-- 位于歌单列表下方的图层 -->
<div class="bg-layer" ref="layer"></div>
<scroll @scroll="scroll" :probe-type="probeType" :listen-scroll="listenScroll" :data="songs" class="list" ref="list">
  <div class="song-list-wrapper">

接下来我们要实现的是scroll层(song-list.vue)滚动的时候,bg-layer层也跟着向上滚动,所以我们要监听scroll层,也就是song-list层滚动的位置

首先在music-list中设置scroll开启监听,之后scroll就会将pos派发出来,然后再用scroll监听事件,获取自定义参数scrollY的值

created() {
  this.probeType = 3
  this.listenScroll = true // 监听滚动
},

然后将变量传到scroll中,设置监听方法scroll,因为只要listenScroll为ture,scroll中就会派发函数,music-list中就可以监听到scroll的pos

<scroll @scroll="scroll" :probe-type="probeType" :listen-scroll="listenScroll" :data="songs" class="list" ref="list">

在scroll中initScroll的时候已经定义好了派发函数

// 如果监听了滚动事件,在初始化列BScroll之后要派发一个监听事件
if (this.listenScroll) {
  // BScroll 中的this是默认指向scroll的,所以要在me中保留vue实例的this
  let me = this
  // 当我触发滚动事件的时候就向父组件listview.vue,music-list.vue派发一个名为scroll的事件,还带有参数pos
  this.scroll.on('scroll', (pos) => {
    me.$emit('scroll', pos)
  })
}

这样,我们只需要在music-list.vue组件中编写监听函数,获得scrollY的位置即可

data() {
  return {
    scrollY: 0
  }
},
methods: {
  scroll(pos) {
    this.scrollY = pos.y // 实时拿到scrollY的值
  },

拿到scrollY的值之后就可以设置bg-layer的偏移量

监听scrollY的变化,scrollY发生变化时,bg-layer层跟着滚动scrollY的位置

watch: {
        scrollY(newY) {
            this.$refs.layer.style['transform'] = `translate3D(0, ${newY}px, 0)`
            this.$refs.layer.style['webkitTransform'] = `translate3D(0, ${newY}px, 0)`
        }
}

bg-layer层高度是100%的,当by-layer滚动高度的超过屏幕高度时,图片又会露出来。bg-layer应该滚动到一定位置就停止,所以要设置一下他的最大滚动距离,在mounted中设置一个最小滚动距离,在watch的时候当scrollY超过最小滚动距离时就不动了
首先在mounted中记录一下最大滚动高度imageHeight

const RESERVED_HEIGHT = 40
mounted() {
  // 记录一下bg-layer的最大滚动位置
  this.imageHeight = this.$refs.bgImage.clientHeight
  // 最小可以滚动到的位置,这是一个负值,并为顶部留出了一个text的空间
  this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT
  // list是一个component对象,要通过$el取得它的DOM,将scoll的高度设置为背景图的高度
  this.$refs.list.$el.style.top = `${this.imageHeight}px`
},

然后在watch中观测到scrollY值变化时就就设置layer层跟随滚动到一定位置

watch: {
  scrollY(newY) {
    // 当newY大于最小滚动值的时候就把高度设为最小滚动值,因为他们都是负值,所以取最大的值,其绝对值最小
    let translateY = Math.max(this.minTranslateY, newY)
    this.$refs.layer.style[transform] = `translate3d(0, ${translateY}px, 0)`
    this.$refs.layer.style['webkitTransform'] = `translate3D(0, ${translateY}px, 0)`

我们希望文字在达到预留text窗口时隐藏,也就是歌单的名字不会盖住歌手的名字和返回按钮。
原来歌手bg-image图片的布局也是relative布局的,我们可以为其设置一个zIndex=10。但是又会出现一个问题,就是图片部分完全盖住了歌单列表,也就是bg-layer滚动到最大距离并且还没有盖住歌手名字之前,我们改变其z-index和高度,没有滚动到最大高度时和之前一样就行。
原来的图片布局是靠padding-top来支撑的

.bg-image
  position: relative
  width: 100%
  height: 0
  padding-top: 70%

所以在watch scrollY的时候,我们对图片的高度和index做一个限定

let zIndex = 0
if (newY < this.minTranslateY) {
  // 滚动到顶时
  zIndex = 10
  this.$refs.bgImage.style.paddingTop = 0 // 这两句直接将高度写死就成
  this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px` // 预留一个title空间
} else {
  // 还没滚动到最高点时要把它重置回去,因为列表拉上去在拉下来的话图片部分就没有了,还是只露出上边一小部分
  this.$refs.bgImage.style.paddingTop = '70%'
  this.$refs.bgImage.style.height = 0
}
this.$refs.bgImage.style.zIndex = zIndex // if中为10,else中就不变还是为0

拉动歌曲列表的时候,实现图片的一个放大缩小(scale)的效果和模糊度的效果(blur)
首先watch scrollY的时候,先定义一个scale为1
向下拉的时候,我们希望有一个高斯模糊的效果,往上滑的时候越高它的模糊度越大,定义一个blur

let zIndex = 0
let scale = 1
let blur = 0
const percent = Math.abs(newY / this.imageHeight)
if (newY > 0) { // 向下拉的时候
  scale = 1 + percent // 相当于加了一个nweY的高度
  // 向下拉的时候图片被盖住一部分,设置其index大于歌名列表
  zIndex = 10
} else {
  blur = Math.min(2 * percent, 20)
}
this.$refs.filter.style[backdrop-filter] = `blur(${blur}px)` // 模糊
this.$refs.filter.style[webkitBackdrop-filter] = `blur(${blur}px)`
this.$refs.bgImage.style[transform] = `scale(${scale})`
this.$refs.bgImage.style[webkitTransform] = `scale(${scale})`

对transition等css属性进行封装,在common/js/dom.js中进行扩展,以兼容浏览器

// 创建了一个div的style
let elementStyle = document.createElement('div').style

// 立即执行函数,返回一个浏览器类型
let vendor = (() => {
  // 各个浏览器厂商的前缀
  let transformNames = {
    webkit: 'webkitTransform',
    Moz: 'MozTransform',
    O: 'OTransform',
    ms: 'msTransform',
    standard: 'transform'
  }

  for (const key in transformNames) {
    if (elementStyle[transformNames[key]] !== undefined) {
      return key
    }
  }

  return false // 所有的类型都不支持的返回false
})()

/**
 * css3属性添加前缀
 * @export
 * @param {any} style 样式
 * @returns 前缀+style
 */
export function prefixStyle(style) {
  if (vendor === false) {
    return false
  }

  if (vendor === 'standard') {
    return style
  }
  // 首字母大写再加上剩余部分 例如:webkitTransform
  return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

所以在music-list中引入:

import {prefixStyle} from 'common/js/dom'

const transform = prefixStyle('transform')
const backdrop = prefixStyle('backdrop-filter')

这样,就可以替换css style中的一下兼容性代码了(webkit-transform),他可以自动根据我们浏览器的兼容情况自动添加前缀
制作返回按钮

<div class="back" @click="back">
  <i class="icon-back"></i>
</div>

methods: {
  back() {
    this.$router.back()
  },

添加随机播放的按钮,在bg-image层中添加,添加逻辑v-show,保证歌曲列表渲染完成才会出现随机播放按钮

<div class="bg-image" :style="bgStyle" ref="bgImage">
  <div class="play-wrapper">
    <div class="play" v-show="songs.length > 0" ref="playBtn" >
      <i class="icon-play">
        <span class="text">随机播放全部</span>
      </i>
    </div>

按钮的出现应该在数据渲染完成之后,这里我们为按钮添加一个v-show的条件,如上所示 v-show=”songs.length > 0”
play-wrapper是一个绝对定位,当我们滚动列表的时候,按钮也会滚动到页面的顶部,滚动到顶部的时候,我们实际上是修改了bg-image的高度

if (newY < this.minTranslateY) {
  // 滚动到顶时
  zIndex = 10
  this.$refs.bgImage.style.paddingTop = 0 // 这两句直接将高度写死就成
  this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px` // 预留一个title空间
  this.$refs.playBtn.style.display = 'none' // 随机播放全部按钮消失
} else {
  // 还没滚动到最高点时要把它重置回去,因为列表拉上去在拉下来的话图片部分就没有了,还是只露出上边一小部分
  this.$refs.bgImage.style.paddingTop = '70%'
  this.$refs.bgImage.style.height = 0
  this.$refs.playBtn.style.display = '' // 随机播放全部按钮显示出来
}

在scroll的最后添加loading组件,不要忘了import和components

<div class="loading-container" v-show="!songs.length">
  <loading></loading>
</div>

引入、注册Loading组件

import Loading from 'base/loading/loading'
components: {
  Scroll,
  SongList,
  Loading
}