• 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; }
  • 相关阅读:
    Sql例子Sp_ExecuteSql 带参数
    Flex显示麦克风当前音量
    无法将 flash.display::Sprite@156b7b1 转换为 mx.core.IUIComponent
    FMS (端口问题)如何穿透防火墙
    19:A*B问题
    6264:走出迷宫
    2753:走迷宫
    1792:迷宫
    换钱问题(经典枚举样例)
    1943(2.1)
  • 原文地址:https://www.cnblogs.com/Small-Windmill/p/14977300.html
Copyright © 2020-2023  润新知