1. 程式人生 > 其它 >VUE移動端音樂APP學習【二十五】:歌曲列表元件開發(二)

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