zoukankan      html  css  js  c++  java
  • VUE移动端音乐APP学习【二十四】:歌曲列表组件开发(一)

    点击迷你播放器的列表按钮就会弹出一个当前播放的歌曲列表层,这个列表也有一些功能,比如播放模式的控制,点击歌曲播放,收藏歌曲以及从列表中删除歌曲,点击垃圾桶把歌曲列表清空,甚至还可以添加歌曲到队列。

    首先是对首页进行开发,基本代码如下:

    <template>
      <transition name="list-fade">
        <div class="playlist">
          <div class="list-wrapper">
            <div class="list-header">
              <h1 class="title">
                <i class="icon"></i>
                <span class="text"></span>
                <span class="clear"><i class="iconfont icon-clear"></i></span>
              </h1>
            </div>
            <div class="list-content">
              <ul>
                <li class="item">
                  <i class="current"></i>
                  <span class="text"></span>
                  <span class="like">
                    <i class="iconfont icon-not-favorite"></i>
                  </span>
                  <span class="delete">
                    <i class="iconfont icon-delete"></i>
                  </span>
                </li>
              </ul>
            </div>
            <div class="list-operate">
              <div class="add">
                <i class="iconfont icon-add"></i>
                <span class="text">添加歌曲到队列</span>
              </div>
            </div>
            <div class="list-close">
              <span>关闭</span>
            </div>
          </div>
        </div>
      </transition>
    </template>
    
    <script>
    export default {
      data() {
        return {
          show: true,
        };
      },
    };
    </script>
    
    <style lang="scss"  scoped >
    .playlist {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 200;
      background: $color-background-d;
    
      &.list-fade-enter-active,
      &.list-fade-leave-active {
        transition: opacity 0.3s;
    
        .list-wrapper {
          transition: all 0.3s;
        }
      }
    
      &.list-fade-enter,
      &.list-fade-leave-to {
        opacity: 0;
    
        .list-wrapper {
          transform: translate3d(0, 100%, 0);
        }
      }
    
      .list-wrapper {
        position: absolute;
        left: 0;
        bottom: 0;
        width: 100%;
        background-color: $color-highlight-background;
    
        .list-header {
          position: relative;
          padding: 20px 30px 10px 20px;
    
          .title {
            display: flex;
            align-items: center;
    
            .icon {
              margin-right: 10px;
              font-size: 30px;
              color: $color-theme-d;
            }
    
            .text {
              flex: 1;
              font-size: $font-size-medium;
              color: $color-text-l;
            }
    
            .clear {
              @include extend-click();
    
              .icon-clear {
                font-size: $font-size-medium;
                color: $color-text-d;
              }
            }
          }
        }
    
        .list-content {
          max-height: 240px;
          overflow: hidden;
    
          .item {
            display: flex;
            align-items: center;
            height: 40px;
            padding: 0 30px 0 20px;
            overflow: hidden;
    
            .current {
              flex: 0 0 20px;
              width: 20px;
              font-size: $font-size-small;
              color: $color-theme-d;
            }
    
            .text {
              flex: 1;
    
              @include no-wrap();
    
              font-size: $font-size-medium;
              color: $color-text-d;
            }
    
            .like {
              @include extend-click();
    
              margin-right: 15px;
              font-size: $font-size-small;
              color: $color-theme;
    
              .icon-favorite {
                color: $color-sub-theme;
              }
            }
    
            .delete {
              @include extend-click();
    
              font-size: $font-size-small;
              color: $color-theme;
            }
          }
        }
    
        .list-operate {
          width: 140px;
          margin: 20px auto 30px auto;
    
          .add {
            display: flex;
            align-items: center;
            padding: 8px 16px;
            border: 1px solid $color-text-l;
            border-radius: 100px;
            color: $color-text-l;
    
            .icon-add {
              margin-left: 5px;
              font-size: $font-size-small-s;
              padding-right: 5px;
            }
    
            .text {
              font-size: $font-size-small;
            }
          }
        }
    
        .list-close {
          text-align: center;
          line-height: 50px;
          background: $color-background;
          font-size: $font-size-medium-x;
          color: $color-text-l;
        }
      }
    }
    </style>
    playlist.vue

    在player引入该组件

    <transition class="mini" >
    </transition>
    <playlist></playlist>
    
    import Playlist from '../playlist/playlist.vue';
    
    components: {
        ProgressBar,
        ProgressCircle,
        Scroll,
        Playlist,
      },

     由外层来控制playlist的显示,首先在playlist定义showFlag来判断它的显示并提供2个方法

        <div class="playlist" v-show="showFlag">
    
    
     data() {
        return {
          showFlag: false,
        };
      },
    
     methods: {
        show() {
          this.showFlag = true;
        },
        hide() {
          this.showFlag = false;
        },
      },

    外层就可以通过这2个方法来控制playlist的显示和隐藏,当点击歌曲列表图标的时候就可以让它显示,所以还要给歌曲列表图片添加点击事件。

     <div class="control" @click="showPlaylist">
            <i class="iconfont icon-playlist"></i>
     </div>
    
    
    <playlist ref="playlist"></playlist>
    
    
     showPlaylist() {
          this.$refs.playlist.show();
        },

    当点击歌曲列表下方的关闭则让它隐藏。在playlist的整个朦胧层和关闭添加点击事件“hide”。同时还需要在朦胧层的子组件阻止其冒泡,否则点击内部内容时也会隐藏。

    <div class="playlist" v-show="showFlag" @click="hide">
          <div class="list-wrapper" @click.stop>
            ......
    
             <div class="list-close" @click="hide">
              <span>关闭</span>
            </div>
           </div>
    </div>

    接下来就是显示播放列表的数据,通过vuex拿到数据然后在dom上遍历。

    import { mapGetters } from 'vuex';
    
    computed: {
        ...mapGetters([
          'sequenceList',
        ]),
      },
    <div class="list-content">
              <ul>
                <li class="item" v-for="(item,index) in sequenceList" :key="index">
                  <i class="current"></i>
                  <span class="text">{{item.name}} - {{item.singer}}</span>
                  <span class="like">
                    <i class="iconfont icon-not-favorite"></i>
                  </span>
                  <span class="delete">
                    <i class="iconfont icon-delete"></i>
                  </span>
                </li>
              </ul>
    </div>

     当列表很长的时候,就需要添加scroll组件让它可以滚动。引入scroll组件将list-content的div改为scroll,同时在show的时候重新计算scroll的高度,这样就可以保证dom在渲染了以后调用refresh重新计算的高度是正确的。

     <scroll :data="sequenceList" class="list-content" ref="listContent">
    
     show() {
          this.showFlag = true;
          setTimeout(() => {
            this.$refs.listContent.refresh();
          }, 20);
        },

    有了数据之后我们还要给当前正在播放的歌曲添加样式。

    思路:通过vuex获得的currentSong的id与遍历的item的id进行比较,若id相同则为当前正在播放的歌曲并添加样式

     <i class="iconfont current" :class="getCurrentIcon(item)"></i>
    
    computed: {
        ...mapGetters([
          'sequenceList',
          'currentSong',
        ]),
      },
    
    
     getCurrentIcon(item) {
          if (this.currentSong.id === item.id) {
            return 'icon-play';
          }
          return '';
        },

     有了这个播放按钮,再去点击别的列表元素,希望播放图标能切到对应的位置。

    首先,需要获得播放模式类型,还有从vuex获得播放列表playlist以及currentIndex的mutations

    import { playMode } from '../../common/js/config';
    import { mapGetters, mapMutations } from 'vuex';
      computed: {
        ...mapGetters([
          'sequenceList',
          'currentSong',
          'playlist',
          'mode',
        ]),
      },
    
     ...mapMutations({
          setCurrentIndex: 'SET_CURRENT_INDEX',
        }),

    然后给li元素添加点击事件selectItem(item,index):根据播放模式设置currentIndex然后调用mutation去set currentIndex

     <li class="item" v-for="(item,index) in sequenceList" :key="index" @click="selectItem(item,index)">

    可以看到播放列表的图标及播放列表的歌曲可以切换了,但是在暂停时切换歌曲,歌曲在播放但是播放状态却显示为暂停。所以需要设置下播放状态

    selectItem(item, index) {
          // 调用mutation去set currentIndex
          // 播放模式为随机播放的话,index需要重新设置
          if (this.mode === playMode.random) {
            // 找到当前元素在playlist的索引
            index = this.playlist.findIndex((song) => {
              return song.id === item.id;
            });
          }
          this.setCurrentIndex(index);
          this.setPlayingState(true);
        },
    ...mapMutations({
          setCurrentIndex: 'SET_CURRENT_INDEX',
          setPlayingState: 'SET_PLAYING_STATE',
        }),

    优化:每次点击播放列表时,player的背景也会弹上来。因为这个playlist组件的父容器也有一个click事件,所以就冒泡到父容器player上。所以要加个.stop阻止冒泡

     <div class="control" @click.stop="showPlaylist">
              <i class="iconfont icon-playlist"></i>
     </div>

     实现列表滚动到当前播放歌曲的功能:

    在playlist组件定义scrollToCurrent方法

     scrollToCurrent(current) {
          // 找到当前元素current在sequenceList的索引
          const index = this.sequenceList.findIndex((song) => {
            return current.id === song.id;
          });
          // 根据索引滚动对应的列表元素
          this.$refs.listContent.scrollToElement(this.$refs.listItem[index], 300);
        },

    当我们歌曲切换成功的时候就可以滚动,这需要使用watch观测currentSong的变化;除此之外每次点击playlist显示的时候也要滚动到当前播放歌曲。

     show() {
          this.showFlag = true;
          setTimeout(() => {
            this.$refs.listContent.refresh();
            this.scrollToCurrent(this.currentSong);
          }, 20);
        },
    
    
    watch: {
        currentSong(newSong, oldSong) {
          // 如果组件不显示或者歌曲没有被切换
          if (!this.showFlag || newSong.id === oldSong.id) {
            return;
          }
          this.scrollToCurrent(newSong);
        },
      },

    点击叉号实现从歌曲列表删除所选元素的功能:

    • 首先给叉号添加点击事件(.stop是因为父容器也有click,防止冒泡)
     <span class="delete" @click.stop="deleteOne(item)">
             <i class="iconfont icon-delete"></i>
    </span>
    • 在vuex中添加删除歌曲的action
    export const deleteSong = function ({ commit, state }, song) {
      let playlist = state.playlist.slice();
      let sequenceList = state.sequenceList.slice();
      let { currentIndex } = state;
      // 找到被删元素在playlist的索引
      let pIndex = findIndex(playlist.song);
      // playlist通过索引删除元素
      playlist.splice(pIndex, 1);
      // 找到被删元素在sequenceList的索引
      let sIndex = findIndex(sequenceList, song);
      // sequenceList通过索引删除元素
      sequenceList.splice(sIndex, 1);
      // 删除完之后需要做个判断:删除元素的索引是否在当前索引之后,如果在前则currentIndex要--;还有一种情况是删除的是最后一首歌
      if (currentIndex > pIndex || currentIndex === playlist.length) {
        currentIndex--;
      }
      // 提交mutation
      commit(types.SET_PLAYLIST, playlist);
      commit(types.SET_SEQUENCE_LIST, sequenceList);
      commit(types.SET_CURRENT_INDEX, currentIndex);
      // 如果删完列表长度为空
      if (!playlist.length) {
        // 把playingState置为false
        commit(types.SET_PLAYING_STATE, false);
      } else {
        // 设置播放状态
        commit(types.SET_PLAYING_STATE, true);
      }
    };
    • 在点击事件中调用它
    deleteOne(item) {
          this.deleteSong(item);
        },
    
    ...mapActions([
          'deleteSong',
        ]),
    • 当删除完所有歌曲后,再点击一首歌曲后会有报错并且歌曲列表自动展示。

    自动展示原因:playlist是在player组件里的,当歌曲列表长度为0的时候,player的v-show效果为隐藏,但是实际上playlist组件还是为显示状态,所以当点击一首歌曲后,它就自动弹出显示了。

    解决方法:在删除歌曲后判断它长度是否为0,为0则调用hide方法将其隐藏。

      deleteOne(item) {
          this.deleteSong(item);
          if (!this.playlist.length) {
            this.hide();
          }
        },

    报错原因:删除歌曲的时候修改了playlist以及currentIndex,这样就导致了currentSong发生了变化。在player组件里有个watch,它会watch currentSong的变化。列表中已经没有歌曲了,currentSong(newSong,oldSong)的newSong实际上为空的object,显示为defined。

    解决方法:对newSong做边界条件的判断,当为空object时不会继续执行下面的逻辑。

    watch: {
        currentSong(newSong, oldSong) {
          if (!newSong.id) {
            return;
          }
          if (newSong.id === oldSong.id) {
            return;
          }
          if (this.currentLyric) {
            this.currentLyric.stop();
          }
          setTimeout(() => {
            this.$refs.audio.play().catch((error) =>  {
              this.togglePlaying();
              // eslint-disable-next-line no-alert
              alert('播放出错,暂无该歌曲资源');
            }, 1000);
            // 这里不直接调用currentSong.getLyric()
            this.getLyric();
          });
        },
      },

    •  优化:给删除加一下动画,使得不生硬

    把ul替换为transition-group,和transition一样也起一个动画名称,同时指定一个tag属性让它渲染成"ul"

    <transition-group name="list" tag="ul">

    添加动画样式

     &.list-enter-active,
     &.list-leave-active {
              transition: all 0.1s;
            }
    
      &.list-enter,
      &.list-leave-to { height: 0; }
  • 相关阅读:
    Core Animation 文档翻译—附录C(KVC扩展)
    Core Animation 文档翻译—附录B(可动画的属性)
    Core Animation 文档翻译—附录A(Layer样貌相关属性动画)
    Core Animation 文档翻译 (第八篇)—提高动画的性能
    Core Animation 文档翻译 (第七篇)—改变Layer的默认动画
    Core Animation 文档翻译 (第六篇)—高级动画技巧
    Core Animation 文档翻译 (第五篇)—构建Layer的层次结构
    用Markdown快速排版一片文章
    Core Animation 文档翻译 (第四篇)—让Layer的content动画起来
    Core Animation 文档翻译(第三篇)—设置Layer对象
  • 原文地址:https://www.cnblogs.com/Small-Windmill/p/14977300.html
Copyright © 2011-2022 走看看