• 【音乐App】—— Vue-music 项目学习笔记:歌曲列表组件开发


    前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。

    项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。


    当前歌曲播放列表 添加歌曲到队列

    components->playlist目录下:创建playlist.vue -- 在play.vue中应用

    一、.歌曲列表组件显示和隐藏的控制
    • data中维护一个数据 
      showFlag: false
    • 使用v-show判断showFlag控制显示隐藏
      <div class="playlist" v-show="showFlag">
    • methods中分别定义show()和hide(),设置showFlag为true和false
      show() {
          this.showFlag = true
      },
      hide() {
          this.showFlag = false
      }
    • player.vue中:给列表按钮添加点击事件,并阻止事件冒泡,showPlaylist方法执行playlist中的show()控制列表的显示
      <div class="control" @click.stop.prevent="showPlaylist">
      <play-list ref="playlist">
      showPlaylist() {
          this.$refs.playlist.show()
      }
    • playlist.vue中:给列表的蒙层和关闭按钮都添加点击事件,触发hide方法,控制列表的隐藏
    • 坑:点击列表本身也会关闭列表
    • 原因:给列表蒙层添加点击事件时,实际上添加到了最外层<div class="playlist">上,点击子元素时会事件冒泡到最外层
    • 解决:给list-wrapper添加@click.stop,阻止点击事件冒泡 
      <div class="list-wrapper" @click.stop>
    二、歌曲列表组件播放列表的实现
    • 通过mapGetters获取顺序播放的歌曲列表添加到列表项
      import {mapGetters} from 'vuex'
      
      computed: {
          ...mapGetters([
             'sequenceList'
         ])
      }
      <li class="item" v-for="(item, index) in sequenceList" :key="index">
          <span class="text">{{item.name}}</span>
    • 应用Scroll组件实现歌曲列表滚动
      <scroll class="list-content" :data="sequenceList" ref="listContent">
    • 在show()时让scroll重新进行计算,确保当前高度是正确的
      show() {
          this.showFlag = true
          setTimeout(() => {
               this.$refs.listContent.refresh()
          }, 20)
      }
    • 给当前播放的歌曲添加current高亮显示的样式
      <i class="current" :class="getCurrentIcon(item)"></i>

      通过mapGetters获取当前歌曲: 'currentSong'

      getCurrentIcon(item) {
          if(this.currentSong.id === item.id) {
              return 'icon-play'
          }
          return ''
      }
    • 选择列表项播放歌曲
      <li class="item" @click="selectItem(item, index)">

      通过mapGetters获得currentSong和playlist,通过mapMutations调用setCurrentIndex提交数据

      import {playMode} from '@/common/js/config'
      
      computed: {
          ...mapGetters([
               'sequenceList',
               'currentSong',
               'playlist',
               'mode'
         ])
      }
      
      selectItem(item, index){
          //如果当前是随机播放,重新计算index
          if(this.mode === playMode.random) {
             inde = this.playlist.findIndex((song) => {
                   return song.id === item.id
             })
         } 
         this.setCurrentIndex(index)
         this.setPlayingState(true)
      }
      
      ...mapMutations({
         setCurrentIndex: 'SET_CURRENT_INDEX',
         setPlayingState: 'SET_PLAYING_STATE'
      }) 
    • 歌曲列表滚动到当前播放的歌曲
    1. 需求:切换歌曲时,歌曲列表滚动到当前播放的歌曲位置;打开歌曲列表时,当前播放的歌曲始终在第一行显示
    2. 封装一个滚动到当前播放歌曲的方法
      scrollToCurrent(current) {
         //找到当前歌曲在顺序列表中的索引
         const index = this.sequenceList.findIndex((song) => {
               return current.id === song.id
         })
         this.$refs.listContent.scrollToElement(this.$refs.listItem[index], 300)
      }
    3. 切换歌曲成功时调用
      watch: {
          currentSong(newSong, oldSong) {
              if(!this.showFlag || newSong.id === oldSong.id) {
                 return
             }
             this.scrollToCurrent(newSong)
         }
      }
    4. 歌曲列表显示时调用
      show() {
          this.showFlag = true
          setTimeout(() => {
               this.$refs.listContent.refresh()
               this.scrollToCurrent(this.currentSong)
          }, 20)
      }
    • 从歌曲播放列表中删掉所选歌曲
      <span class="delete" @click.stop="deleteOne(item)">
    1. actions.js中:封装deleteSong()
      export const deleteSong = function ({commit, state}, song){
          let playlist = state.playlist.slice() //副本
          let sequenceList = state.sequenceList.slice() //副本
          let currentIndex = state.currentIndex
      
          let pIndex = findIndex(playlist, song)
          playlist.splice(pIndex, 1)
      
          let sIndex = findIndex(sequenceList, song)
          sequenceList.splice(sIndex, 1)
      
          if(currentIndex > pIndex || currentIndex === playlist.length){
             currentIndex--
          }
          commit(types.SET_PLAYLIST, playlist)
          commit(types.SET_SEQUENCE_LIST, sequenceList)
          commit(types.SET_CURRENT_INDEX, currentIndex)
      
          const playingState = playlist.length > 0
          commit(types.SET_PLAYING_STATE,playingState)
      }
    2. 通过mapActions获取deleteSong方法
      ...mapActions([
          'deleteSong'
      ])
    3. methods中定义deleteOne(),调用deleteSong()
      deleteOne(item) {
         this.deleteSong(item)
         if(!this.playlist.length){
            this.hide()
         } 
      }
    4. 坑:报错this.currentSong.getLyric is not a function
    5. 原因: action deleteSong修改了playlist和currentIndex,导致currentSong发生变化;在player.vue中的watch会监测到currentSong变化了,但其实这里列表中已经没有歌曲了;newSong为空的Object,此时将newSong和oldSong进行对比,都会返回undefined,所以报错
    6. 解决:在watch currentSong()中判断如果没有newSong.id,直接返回,不执行任何操作
      if(!newSong.id) {
         return
      }
    • 优化:给删除歌曲添加动画
    1. 将<ul>替换为
      <transition-group name="list" tag="ul">
    2. transition-group的关键: 内容<li class="item">必须要有 :key="index"
      .item
         height: 40px
         &.list-enter-active, &.list-leave-active
            transition: all 0.1s
         &.list-enter, &.list-leave-to
            height: 0
    • 清除歌曲播放列表
    1. 同搜索历史列表,在点击清除按钮后,需要先显示一个confirm弹窗,然后选择确认或取消
    2. 给清空按钮添加点击事件,显示弹窗:
      <span class="clear" @click="showConfirm">
      showConfirm() {
          this.$refs.confirm.show()
      }
    3. 给弹窗监听confirm事件,同时封装action,在confirm()中调用
      <confirm ref="confirm" text="是否清空播放列表" confirmBtnText="清空"
               @confirm="confirmClear"></confirm>
      confirmClear() {
         this.deleteSongList()
         this.hide()
      }
      ...mapActions([
         'deleteSong',
         'deleteSongList'
      ])

      actions.js中:

      export const deleteSongList = function ({commit}){
         //将所有值都重置为初始状态
         commit(types.SET_PLAYLIST, [])
         commit(types.SET_SEQUENCE_LIST, [])
         commit(types.SET_CURRENT_INDEX, -1)
         commit(types.SET_PLAYING_STATE, false)
      }
    4. 坑:点击取消时,歌曲播放列表也会被关闭
    5. 原因:在playlist.vue中<confirm>在<div class="playlist" @click="hide">内,点击confirm会事件冒泡,触发hide
    6. 解决:让confirm组件更加独立,阻止事件冒泡 
      <div class="confirm" v-show="showFlag" @click.stop>
    三、playerMixin的抽象
    • 需求:歌曲播放列表中的播放模式切换功能与播放器中的逻辑相同,可以使用Mixin复用
    • mixin.js中:创建新的playerMixin对象
      import {mapGetters, mapMutations} from 'vuex'
      import {playMode} from '@/common/js/config'
      import {shuffle} from '@/common/js/util'
      
      export const playerMixin = {
          computed: {
              iconMode(){
                  return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === 
      playMode.loop ? 'icon-loop' : 'icon-random'
              },
             ...mapGetters([
                  'sequenceList',
                  'currentSong',
                  'playlist',
                  'mode'
              ])
         },
         methods: {
            changeMode(){
                  const mode = (this.mode + 1) % 3
                  this.setPlayMode(mode)
                  let list = null
                  if(mode === playMode.random){
                     list = shuffle(this.sequenceList)
                  }else{
                     list = this.sequenceList
                  }
                  this.resetCurrentIndex(list)
                  this.setPlayList(list)
           },
           resetCurrentIndex(list){
                  let index = list.findIndex((item) => { //es6语法
                       return item.id === this.currentSong.id
                  })
                  this.setCurrentIndex(index)
           },
           ...mapMutations({
                  setPlayingState: 'SET_PLAYING_STATE',
                  setCurrentIndex: 'SET_CURRENT_INDEX',
                  setPlayMode: 'SET_PLAY_MODE',
                  setPlayList: 'SET_PLAYLIST'
           })
         }
      }
    • player和playlist中:应用playerMixin,同时删掉共用部分
      import {playerMixin} from '@/common/js/mixin'
      mixins:[playerMixin],
    • playlist.vue中:动态绑定class,监听点击事件
      <i class="icon" :class="iconMode" @click="changeMode">
      computed: {
         modeText() {
             return this.mode === playMode.sequence ? '顺序播放' : this.mode === playMode.random ? '随机播放' : '单曲循环'
         }
      }
    四、添加歌曲到列表add-song组件实现
    • components->add-song目录下:创建add-song.vue
    • 页面的显示隐藏:同playlist.vue
    • 搜索框和搜索结果:与search.vue共有一些相同的逻辑,使用mixin复用
    1. mixin.js中创建新的searchMixin对象
      export const searchMixin = {
          computed: {
             ...mapGetters([
               'searchHistory'
            ])
          },
         data() {
            return {
                 query: ''
            }
         },
         methods: {
            blurInput() {
                this.$refs.searchBox.blur()
            },
            saveSearch() {
                this.saveSearchHistory(this.query)
            },
            onQueryChange(query){
                this.query = query
            },
            addQuery(query) {
                this.$refs.searchBox.setQuery(query)
            },
            ...mapActions([
                'saveSearchHistory',
                'deleteSearchHistory'
            ])
         }
      }
    2. search和add-song中:应用searchMixin,同时删掉共用部分
      import {searchMixin} from '@/common/js/mixin'
      
      mixins:[searchMixin],
    3. add-song.vue中:绑定数据,监听事件
      <search-box ref="searchBox" @query="onQueryChange" placeholder="搜索歌曲">
      <suggest :query="query" :showSinger="showSinger" @select="selectSuggest" @listScroll="blurInput">
    • 切换Tab组件
    1. base->switches目录下:创建switches.vue
      <li class="switch-item" v-for="(item, index) in switches" :key="index"
          :class="{'active': currentIndex === index}" @click="switchItem(index)">
          <span>{{item.name}}</span>
      </li>
      props: {
          switches: {
              type: Array,
              default: []
          },
          currentIndex: {
              type: Number,
              default: 0
          }
      },
      methods: {
          switchItem(index) {
              this.$emit('switch', index)
          }
      }
    2. add-song.vue中应用:
      <switches :switches="switches" :currentIndex="currentIndex" @switch="switchItem"></switches>
      currentIndex: 0,
      switches: [
          {name: '最近播放'},
          {name: '搜索历史'}
      ]
      switchItem(index) {
          this.currentIndex = index
      }
    • 最近播放列表
    1. Vuex管理数据

      states.js中:添加数据

      playHistory: []

      mutations-types.js中:定义事件类型常量

      export const SET_PLAY_HISTORY = 'SET_PLAY_HISTORY'

      mutations.js中:创建方法

      [types.SET_PLAY_HISTORY](state, history){
         state.playHistory = history
      }

      getters.js中:定义数据映射

      export const playHistory = state => state.playHistory
    2. player.vue中:当歌曲ready后通过mapActions向Vuex中写入数据
      ready() {
         this.songReady = true
         this.savePlayHistory(this.currentSong)
      }
      ...mapActions([
         'savePlayHistory' 
      ])
    3. catch.js中:实现对本地缓存的操作
      //将歌曲数据保存到本地缓存
      export function savePlay(song) {
          let songs = storage.get(PLAY_KEY, [])
          insertArray(songs, song, (item) => {
               return item.id === song.id
          }, PLAY_MAX_LENGTH)
          storage.set(PLAY_KEY, songs)
          return songs
      }
      //从本地缓存中取出歌曲数据
      export function loadPlay() {
          return storage.get(PLAY_KEY, [])
      }
    4. state.js中:修复playHistory初始值为当前本地缓存中的数据
      playHistory: loadPlay()
    5. actions.js中:引入savePlay方法将数据同时存入Vuex和本地缓存
      export const savePlayHistory = function({commit}, song){
          commit(types.SET_PLAY_HISTORY, savePlay(song))
      }
    6. add-song.js中:通过mapGetters获取Vuex中的播放历史
      <div class="list-wrapper">
           <scroll class="list-scroll" v-if="currentIndex===0" :data="playHistory">
                <div class="list-inner">
                     <song-list :songs="playHistory"></song-list>
                 </div>
           </scroll>
      </div>
      import Scroll from '@/base/scroll/scroll'
      import {mapGetters} from 'vuex'
      import SongList from '@/base/song-list/song-list'
      
      computed: {
          ...mapGetters([
             'playHistory'
         ])
      }
    7. add-song.js中:监听select事件,通过mapActionis从播放历史中选择歌曲插入播放列表
      @select="selectSong"
      import Song from '@/common/js/song'
      
      selectSong(song, index) {
         if(index !== 0) {
            //从playHistory中获取到的song还是一个对象,需要实例化为Song类
            this.insertSong(new Song(song))
         }
      },
      ...mapActions([
         'insertSong'
      ])
    8. 搜索历史列表:复用SearchList组件以及searchMixin中的数据和方法
      <scroll ref="searchList" class="list-scroll" v-if="currentIndex===1" 
              :data="searchHistory">
             <div class="list-inner">
                  <search-list @delete="deleteSearchHistory" @select="addQuery"
                               :searches="searchHistory"></search-list>
             </div>
      </scroll>
    9. 优化:在search-list.vue中通过transition-group给删除列表时添加动画
    10. 关键:<transition-group>中的元素一定要有key值区分元素间的不同
    11. 优化:在添加歌曲到列表页面显示时,判断当前显示列表,对应scroll重新计算,确保高度正确
      show() {
         this.showFlag = true
         setTimeout(() => {
            if(this.currentIndex === 0){
               this.$refs.songList.refresh()
            }else{
               this.$refs.searchList.refresh()
            }
        })
      }
    • 顶部提示框
    1. base->top-list目录下:创建top-list.vue
      <transition name="drop">
         <div class="top-tip" v-show="showFlag" @click.stop="hide">
              <slot></slot>
         </div> 
      </transition>

      通过showFlag控制显示隐藏,同play-list.vue

    2. add-song.vue中:应用top-list,添加slot
      <top-tip ref="topTip">
          <div class="tip-title">
               <i class="icon-ok"></i>
               <span class="text">1首歌曲已经添加到播放队列</span>
          </div>
      </top-tip>

      分别在selectSuggest()和selectSong()中调用showTip()显示提示框

    3. top-list.vue中:在show()内调用定时器,设置提示框显示2s自动关闭
    4. 坑:如果快速的调用show(),会有很多定时器存在
    5. 解决:每次show()时,在调用新的timer前就先清空前面的timer
      props: {
         delay: {
             type: Number,
             default: 2000
         }
      }
      
      show() {
          this.showFlag = true
          clearTimeout(this.timer)
          this.timer = setTimeout(() => {
                this.hide()
           }, this.delay)
      }
    五、歌曲列表组件scroll组件能力的扩展
    • 坑:添加歌曲到列表后,歌曲播放列表的滚动位置不对了
    • 原因:playlist中歌曲列表项的添加了transition-group动画,即添加歌曲后歌曲播放列表的高度需要一个100ms的过程;而外层<scroll :data="sequenceList">在watch到数据的变化后,20ms就重新计算了,计算的高度是不对的
    • 解决:扩展scroll组件,添加props属性refreshDelay,让外部组件可以自定义重新计算的延迟时间扩展后再在外部组件的data中定义refreshDelay:100,最后在<scroll>中传入:refreshDelay="refreshDelay"
      refreshDelay: {
         type: Number,
         default: 20
      }
      
      watch: {
         data() { //监测data的变化
             setTimeout(() => {
                 this.refresh()
             }, this.refreshDelay)
         }
      }
    • 同上:search-list组件中的列表项也添加了transition-group动画;在引用到的search组件和add-song组件中都需要传入:refreshDelay="refreshDelay";因为两个组件复用了searchMixin,所以在searchMixin中定义refreshDelay:100

    注:项目来自慕课网

  • 相关阅读:
    POJ3122贪心或者二分(分蛋糕)
    POJ2118基础矩阵快速幂
    POJ2118基础矩阵快速幂
    POJ1328贪心放雷达
    POJ1328贪心放雷达
    hdu4642博弈(矩阵)
    hdu4642博弈(矩阵)
    POJ1042 贪心钓鱼
    POJ3160强连通+spfa最长路(不错)
    POJ3114强连通+spfa
  • 原文地址:https://www.cnblogs.com/ljq66/p/10183504.html
Copyright © 2020-2023  润新知