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

【音乐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

注:项目来自慕课网

posted @ 2018-12-27 16:46 柳洁琼Elena 阅读( ...) 评论( ...) 编辑 收藏
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值