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

    • 实现点击垃圾桶,清除所有歌曲列表功能

    和之前清除所有搜索历史列表一样,引用confirm组件拦截提示用户的操作

     <confirm ref="confirm" text="是否清空播放列表" confirmBtnText="清空"></confirm>

    给垃圾桶图标添加点击事件showConfirm

    <span class="clear" @click="showConfirm"><i class="iconfont icon-clear"></i></span>
    
    
    
        showConfirm() {
          this.$refs.confirm.show();
        },

    在vuex中做清除歌曲的action

    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);
    };

    在confirm也要定义一个事件confrimClear:如果点击弹出框的情况按钮就会调用这个action方法

    <confirm ref="confirm" @confirm="confirmClear" text="是否清空播放列表" confirmBtnText="清空"></confirm>
    
    
    
        confirmClear() {
          this.deleteSongList();
          this.hide();
        },

     如果点击取消,整个列表就关闭了,因为在confirm.vue中有个click事件,confirm被playlist包裹,所以click事件就冒泡到@click="hide"上。为了让confirm的点击事件独立不影响外部,添加@click.stop阻止向上冒泡。

        <div class="confirm" v-show="showFlag" @click.stop>

    • 实现左上角修改播放模式的功能,可以发觉到与player里有许多相同的逻辑,需要使用mixin复用共享两个组件相同的js逻辑

    在mixin.js中创建

    export const playerMixin = {
      computed: {
        iconMode() {
          return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random';
            },
        }
    }

    在player组件和playlist组件使用mixin,player里的iconMode()就可以删掉了。

    import { playerMixin } from '../../common/js/mixin';
    
    
    
    
     mixins: [playerMixin],

    有了mixin后,playlist就可以使用它添加iconMode

    <div class="list-header">
              <h1 class="title">
                <i class="icon iconfont" :class="iconMode"></i>
                ……

     除了这个iconMode的样式需要共享,点击事件也需要共享。将player组件里的有关changeMode的操作方法以及引入方法和除了full_screen之外的mutations和playlist引入相同的mapGetters都拷贝到playerMixin里。有了mixin,两个组件就可以删掉共享的mapGetters和mapMutations,留下特有的。

    export const playerMixin = {
      computed: {
        iconMode() {
          return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random';
        },
    
        ...mapGetters([
          'sequenceList',
          'playlist',
          'currentSong',
          'mode',
          'favoriteList',
        ]),
      },
      methods: {
        changeMode() {
          // 有3种播放模式,每点击一次就改变它的mode
          const mode = (this.mode + 1) % 3;
          this.setPlayMode(mode);
          let list = null;
          if (this.mode === playMode.random) {
            list = shuffle(this.sequenceList);
          } else {
            // 如果是顺序播放或者循环播放
            list = this.sequenceList;
          }
          this.resetCurrentIndex(list);
          this.setPlaylist(list);
        },
        resetCurrentIndex(list) {
          let index = list.findIndex((item) => {
            return item.id === this.currentSong.id;
          });
          this.setCurrentIndex(index);
        },
        ...mapMutations({
          setPlayMode: 'SET_PLAY_MODE',
          setPlaylist: 'SET_PLAYLIST',
          setCurrentIndex: 'SET_CURRENT_INDEX',
          setPlayingState: 'SET_PLAYING_STATE',
        }),
      },
    };

    给图标位置添加点击事件,就可以看到切换模式和player的互相对应

    <i class="icon iconfont" :class="iconMode" @click="changeMode"></i>

    给图标位置旁边添加播放模式名称的文案,因为是playlist特有的,所以写在playlist组件里。

    <i class="icon iconfont" :class="iconMode" @click="changeMode"></i>
    <span class="text">{{modeText}}</span>
    <span class="clear" @click="showConfirm"><i class="iconfont icon-clear"></i></span>
     computed: {
        modeText() {
          return this.mode === playMode.sequence ? '顺序播放' : this.mode === playMode.random ? '随机播放' : '单曲循环';
        },
      },

    •  完成添加歌曲到队列的页面:点击按钮,页面想左滑入,盖住原有的页面。

    创建add-song组件,基本代码如下:

    <template>
      <transition name="slide">
        <div class="add-song">
          <div class="header">
            <h1 class="title">添加歌曲到列表</h1>
            <div class="close">
              <i class="iconfont icon-close"></i>
            </div>
          </div>
          <div class="search-box-wrapper"></div>
          <div class="shortcut"></div>
          <div class="search-result"></div>
        </div>
      </transition>
    </template>
    
    <script>
    export default {
    
    };
    </script>
    
    <style lang="scss">
    .add-song {
      position: fixed;
      top: 0;
      bottom: 0;
      width: 100%;
      z-index: 200;
      background: $color-background;
    
      &.slide-enter-active,
      &.slide-leave-active {
        transition: all 0.3s;
      }
    
      &.slide-enter,
      &.slide-leave-to {
        transform: translate3d(100%, 0, 0);
      }
    
      .header {
        position: relative;
        height: 44px;
        text-align: center;
    
        .title {
          line-height: 44px;
          font-size: $font-size-large;
          color: $color-text;
        }
    
        .close {
          position: absolute;
          top: 0;
          right: 8px;
    
          .icon-close {
            display: block;
            padding: 12px;
            font-size: 20px;
            color: $color-theme;
          }
        }
      }
    
      .search-box-wrapper {
        margin: 20px;
      }
    
      .shortcut {
        .list-wrapper {
          position: absolute;
          top: 165px;
          bottom: 0;
          width: 100%;
    
          .list-scroll {
            height: 100%;
            overflow: hidden;
    
            .list-inner {
              padding: 20px 30px;
            }
          }
        }
      }
    
      .search-result {
        position: fixed;
        top: 124px;
        bottom: 0;
        width: 100%;
      }
    
      .tip-title {
        text-align: center;
        padding: 18px 0;
        font-size: 0;
    
        .icon-ok {
          font-size: $font-size-medium;
          color: $color-theme;
          margin-right: 4px;
        }
    
        .text {
          font-size: $font-size-medium;
          color: $color-text;
        }
      }
    }
    </style>
    add-song.vue

    使用v-show控制该组件的显示隐藏,并向外提供show()方法和hide()方法

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

    在playlist引入add-song,在添加歌曲到队列这部分绑定点击事件方法,在方法中调用add-song提供的show方法让它显示。

     <div class="add" @click="addSong">
    ......
    <add-song ref="addSong"></add-song>
    
      addSong() {
          this.$refs.addSong.show();
        },

    同理,add-song在playlist组件中,任何的点击事件都会冒泡到playlist上,我们要阻止它冒泡。这样它点击页面的任何地方都不会消失,点击叉号就可以隐藏。

    <div class="add-song" v-show="showFlag" @click.stop>

    • 实现添加页面里的搜索框组件

    在add-song组件里引入search-box组件

    <div class="search-box-wrapper">
            <search-box placeholder="搜索歌曲"></search-box>
    </div>
    
    
    import SearchBox from '../../base/search-box/search-box.vue';
    
    
      components: {
        SearchBox,
      },

    search-box监听query事件,add-song组件要维护数据query

    <search-box @query="search" placeholder="搜索歌曲"></search-box>
    data() {
        return {
          showFlag: false,
          query: '',
        };
      },
    
      methods: {
      ……
        search(query) {
          this.query = query;
        },
      },

    根据query就可以决定short-cut和search-result两个区块的显示隐藏

          <div class="shortcut" v-show="!query"></div>
          <div class="search-result" v-show="query"></div>

    search-result区块实际是用来包裹suggest组件,引入suggest组件,然后把query传进去。

          <div class="search-result" v-show="query">
            <suggest :query="query"></suggest>
          </div>
    
    
    import Suggest from '../suggest/suggest.vue';
      components: {
        SearchBox,
        Suggest,
      },

     当我们点击列表元素的时候要做一些处理,与search组件的js逻辑有很多是共用的。这里也定义与search相关的mixin

            <suggest :query="query" @select="selectSuggest"></suggest>
    export const searchMixin = {
      data() {
        return {
          query: '',
        };
      },
      computed: {
        ...mapGetters([
          'searchHistory',
        ]),
      },
      methods: {
        onQueryChange(query) {
          this.query = query;
        },
        blurInput() {
          this.$refs.searchBox.blur();
        },
        addQuery(query) {
          this.$refs.searchBox.setQuery(query);
        },
        saveSearch() {
          this.saveSearchHistory(this.query);
        },
        ...mapActions([
          'saveSearchHistory',
          'deleteSearchHistory',
        ]),
      },
    };

    引入searchMixin,在search组件使用并剔除掉已有的重复内容,在add-song组件使用searchMixin的内容

    //add-song.vue
    <div class="search-box-wrapper">
            <search-box @query="onQueryChange" placeholder="搜索歌曲" @listScroll="blurInput"></search-box>
    </div>
    
    
    import { searchMixin } from '../../common/js/mixin';
      mixins: [searchMixin],

    在selectSuggest方法中调用searchMixin里的saveSearch方法保存搜索历史

        selectSuggest() {
          this.saveSearch();
        },
    • 实现添加页面里的基础组件switches

    创建switches.vue,基本代码如下:

    <template>
      <ul class="switches">
        <li class="switch-item">
          <span></span>
        </li>
      </ul>
    </template>
    
    <script>
    export default {
    
    };
    </script>
    
    <style lang="scss">
    .switches {
      display: flex;
      align-items: center;
      width: 240px;
      margin: 0 auto;
      border: 1px solid $color-hightlight-background;
      border-radius: 5px;
    
      .switch-item {
        flex: 1;
        padding: 8px;
        text-align: center;
        font-size: $font-size-medium;
        color: $color-text-d;
    
        &.active {
          background: $color-highlight-background;
          color: $color-text;
        }
      }
    }
    </style>

    设置这个组件的props

     props: {
      // 标题
        switches: {
          type: Array,
          // eslint-disable-next-line vue/require-valid-default-prop
          default: [],
        },
        // 索引
        currentIndex: {
          type: Number,
          default: 0,
        },
      },

    有了props就可以写dom上的结构了:遍历switches数组显示switches各个标题和根据当前的索引激活高亮

    <template>
      <ul class="switches">
        <li class="switch-item" v-for="(item,index) in switches" :key="index" :class="{'active':currentIndex === index}">
          <span>{{item.name}}</span>
        </li>
      </ul>
    </template>

    在add-song组件引入switches组件,定义currentIndex和switches,然后传给switches组件

    <div class="shortcut" v-show="!query">
            <switches :switches="switches" :currentIndex="currentIndex"></switches>
    </div>
    
    data() {
        return {
          showFlag: false,
          currentIndex: 0,
          switches: [
            { name: '最近播放' },
            { name: '搜索历史' },
          ],
        };
      },

     currentIndex默认为0,所以“最近播放”显示是为高亮的。需要实现点击“搜索历史”,应该把这个currentIndex切到1的点击事件。

    给元素添加点击事件,当被点击时它就派发事件告诉外组件“我被点击了”同时把被点击的索引传给外组件。

    <li class="switch-item" v-for="(item,index) in switches" :key="index" :class="{'active':currentIndex === index}" @click="switchItem(index)">
    
    
      methods: {
        switchItem(index) {
          this.$emit('switch', index);
        },
      },

    父组件add-song监听switch,去修改currentIndex

            <switches :switches="switches" :currentIndex="currentIndex" @switch="switchItem"></switches>
    
    
        switchItem(index) {
          this.currentIndex = index;
        },

    •  显示最近播放列表数据:每播放一首歌,都往里面写入数据或者缓存到本地,这个数据也是被各个组件共享的。

    在state下定义播放历史数据:playHisitory

      // 播放历史
      playHistory: [],

    定义mutation-types、mutations和getters

    //mutation-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;

    在player组件ready的时候往playHistory写入数据,这个过程需要调用action

       ready() {
          this.songReady = true;
          this.savePlayHistory(this.currentSong);
        },

    在actions.js定义savePlayHistory:跟之前的搜索历史是一个套路,在cache.js定义对播放列表的读写方法然后在action中调用然后commit

    //cache.js
    
    const PLAY_KEY = '__play__';
    // 存储最近播放的200首歌曲
    const PLAY_MAX_LENGTH = 200;
    
    export function savePlay(song) {
      let songs = storage.get(PLAY_KEY, []);
      insertArray(songs, song, (item) => {
        // 比较函数: 如果song在里面的话,就挪到前面去
        return item.id === song.id;
      }, PLAY_MAX_LENGTH);
      storage.set(PLAY_KEY, songs);
      return songs;
    }
    export function loadPlay() {
      return storage.get(PLAY_KEY, []);
    }
    //有了loadPlay(),初始值也可以从缓存里面读
    
    //state.js
      // 播放历史
      playHistory: loadPlay(),
    //actions.js
    
    export const savePlayHistory = function ({ commit }, song) {
      commit(types.SET_PLAY_HISTORY, savePlay(song));
    };

    一切准备就绪后就可以在add-song组件使用playHistory数据了。

    通过mapGetters就可以在模板上使用playHistory数据。但是因为这个数据很长,应当是个可以滚动的列表,所以还需要引入scroll组件,并且当currentIndex为0时才会显示滚动列表。

    <scroll v-if="currentIndex === 0" :data="playHistory"></scroll>
    
    
    import Scroll from '../../base/scroll/scroll.vue';
    import { mapGetters } from 'vuex';
    
    computed: {
        ...mapGetters([
          'playHistory',
        ]),
      },
    
    components: {
        SearchBox,
        Suggest,
        Switches,
        Scroll,
      },

    scroll组件包裹的元素其实是之前使用过的song-list组件,这里也需要引入使用来展示playHistory数据。

    <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>
    •  有了这样一个列表,可以实现当点击列表的歌曲时,把它插到当前的播放列表中。列表的第一首歌就不用换了,因为就是当前播放的歌曲。

    监听song-list的@select事件:使用之前写的insertSong方法,点击除了第一首以外的歌都可以插入到当前播放列表中。

    import Song from '../../common/js/song';
    
     selectSong(song, index) {
          // 因为song是从缓存中拿出来的,并不是Song的实例,需要转换
          if (index !== 0) {
            this.insertSong(new Song(song));
          }
        },
        ...mapActions([
          'insertSong',
        ]),

    • 开发搜索历史:复用search-list

    在add-song添加可滚动的区块,即搜索历史这个区块。然后绑定数据,searchHistory可以在mixin定义,通过共享拿到数据

              <scroll class="list-scroll" v-if="currentIndex === 1" :data="searchHistory">
                
              </scroll>

    在scroll区块依然有一个div区块包裹着search-list,这个search-list要传入几个东西,监听删除事件调用mixin已经定义好的deleteSearchHistory;点击事件调用addQuery;往searches传入searchHistory数据。

     <scroll 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>

    在删除搜索历史上添加动画:优化search-list组件,给它添加动画

     <transition-group name="list" tag="ul">
          <li @click="selectItem(item)" class="search-item" v-for="(item,index) in searches" :key="index">
            ……
    
    //动画样式
    
        &.list-enter-active,
        &.list-leave-active {
          transition: all 0.1s;
        }
    
        &.list-enter,
        &.list-leave-to {
          height: 0;
        }

    在add-song组件分别对这2个滚动组件在页面渲染的时候作refresh重新计算高度,防止无法滚动

     show() {
          this.showFlag = true;
          setTimeout(() => {
            if (this.currentIndex === 0) {
              this.$refs.songList.refresh();
            } else {
              this.$refs.searchList.refresh();
            }
          }, 20);
        },
    • 当列表里选中一首歌曲添加到播放列表后,在顶部加一个提示框,实现提示交互效果。

    创建基础组件top-tip,基本代码如下:

    <template>
      <transition name="drop">
        <div class="top-tip">
          <slot></slot>
        </div>
      </transition>
    </template>
    
    <script>
    export default {
    
    };
    </script>
    
    <style lang="scss">
    .top-tip {
      position: fixed;
      top: 0;
      width: 100%;
      z-index: 500;
      background: $color-dialog-background;
    
      &.drop-enter-active,
      &.drop-leave-active {
        transition: all 0.3s;
      }
    
      &.drop-enter,
      &.drop-leave-to {
        transform: translate3d(0, -100%, 0);
      }
    }
    </style>
    top-tip.vue

    使用showFlag变量控制其显示隐藏,并向外提供显示和隐藏的方法

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

    在add-song组件使用它,因为top-tip有一个插件,可以往里面填入内容

    <top-tip ref="topTip">
            <div class="tip-title">
              <i class="iconfont icon-ok"></i>
              <span class="text">1首歌曲已经添加到播放队列</span>
            </div>
    </top-tip>

    定义showTip方法,在selectSuggest和selectSong时调用它控制top-tip的显示

    selectSuggest() {
          this.saveSearch();
          this.showTip();
        },
    
    selectSong(song, index) {
          // 因为song是从缓存中拿出来的,并不是Song的实例,需要转换
          if (index !== 0) {
            this.insertSong(new Song(song));
            this.showTip();
          }
        },
    
    showTip() {
          this.$refs.topTip.show();
        },

    一般这种顶部提示的交互会有几秒钟就可以把它关闭的效果,可以在top-tip组件的show方法添加延时隐藏

     show() {
          this.showFlag = true;
          // 清除定时器,防止多次显示产生多个计时器
          clearTimeout(this.timer);
          this.timer = setTimeout(() => {
            this.hide();
          }, 2000);
        },

    这个2000毫秒可以作为props传入,外部的组件就可以控制top-tip组件的延迟隐藏时间

    props: {
        delay: {
          type: Number,
          default: 2000,
        },
      },
    
    this.timer = setTimeout(() => {
            this.hide();
          }, this.delay);

    在top-tip组件提供另一种隐藏方法:用户点击后隐藏

    <div class="top-tip" v-show="showFlag" @click.stop="hide">

    • 优化:将歌曲添加到播放列表后,播放列表滚动计算的高度不对。scroll组件有:data=“sequenceList",它会watch这个data的变化然后refresh计算高度,但是这里为什么会失效呢。因为playlist中scroll包裹着一个带有动画的列表区块,它的高度有一个缓动的过程。当我们添加或删除歌曲的时候,不是瞬间就增加高度,而是大概有100毫秒的动画后才能得到最终的高度,而scrol组件自设的20毫秒就重新渲染了,计算的高度不对。

    将scroll组件的20毫秒设置为一个变量,作为props传入,外部的组件就可以控制

        refreshDelay: {
          type: Number,
          default: 20,
        },
    
    watch: {
        data() {
          setTimeout(() => {
            this.refresh();
          }, this.refreshDelay);
        },
      },

    playlist向scroll传递一个refreshDelay,除此之外还有search组件和add-song都需要传入一个refreshDelay,由于这2个组件都共用了mixin,可以在mixin的data里定义refreshDelay

            <scroll :data="sequenceList" class="list-content" ref="listContent" :refreshDelay="refreshDelay">
    
      data() {
        return {
          showFlag: false,
          refreshDelay: 100,
        };
      },
  • 相关阅读:
    python常用模块
    清除在Windows下访问共享文件夹时的登录信息
    CentOS 6.3下Samba服务器的安装与配置
    textarea定位光标
    js中apply方法的使用
    js call方法
    linux 安装svn,并设置钩子来同步更新
    mysql给root开启远程访问权限,修改root密码
    在VMware的Linux系统上安装Redis
    (转)NoSQL——Redis在win7下安装配置的学习一
  • 原文地址:https://www.cnblogs.com/Small-Windmill/p/14988647.html
Copyright © 2011-2022 走看看