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方法
<confirmref="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, }; },