前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
当前歌曲播放列表 添加歌曲到队列
components->playlist目录下:创建playlist.vue – 在play.vue中应用
一、.歌曲列表组件显示和隐藏的控制
data中维护一个数据
showFlag: false
使用v-show判断showFlag控制显示隐藏
computed: {
…mapGetters([
‘sequenceList’
])
}
- {{item.name}} 应用Scroll组件实现歌曲列表滚动 在show()时让scroll重新进行计算,确保当前高度是正确的 show() { this.showFlag = true setTimeout(() => { this.$refs.listContent.refresh() }, 20) } 给当前播放的歌曲添加current高亮显示的样式 通过mapGetters获取当前歌曲: 'currentSong'
getCurrentIcon(item) {
if(this.currentSong.id === item.id) {
return ‘icon-play’
}
return ‘’
}
选择列表项播放歌曲import {playMode} from ‘@/common/js/config’
computed: {
…mapGetters([
‘sequenceList’,
‘currentSong’,
‘playlist’,
‘mode’
])
}selectItem(item, index){
//如果当前是随机播放,重新计算index
if(this.mode === playMode.random) {
inde = this.playlist.findIndex((song) => {
return song.id === item.id
})
}
this.setCurrentIndex(index)
this.setPlayingState(true)
}…mapMutations({
setCurrentIndex: ‘SET_CURRENT_INDEX’,
setPlayingState: ‘SET_PLAYING_STATE’
})
歌曲列表滚动到当前播放的歌曲
需求:切换歌曲时,歌曲列表滚动到当前播放的歌曲位置;打开歌曲列表时,当前播放的歌曲始终在第一行显示
封装一个滚动到当前播放歌曲的方法
scrollToCurrent(current) {
//找到当前歌曲在顺序列表中的索引
const index = this.sequenceList.findIndex((song) => {
return current.id === song.id
})
this. r e f s . l i s t C o n t e n t . s c r o l l T o E l e m e n t ( t h i s . refs.listContent.scrollToElement(this. refs.listContent.scrollToElement(this.refs.listItem[index], 300)
}
切换歌曲成功时调用
watch: {
currentSong(newSong, oldSong) {
if(!this.showFlag || newSong.id === oldSong.id) {
return
}
this.scrollToCurrent(newSong)
}
}
歌曲列表显示时调用
show() {
this.showFlag = true
setTimeout(() => {
this.$refs.listContent.refresh()
this.scrollToCurrent(this.currentSong)
}, 20)
}
从歌曲播放列表中删掉所选歌曲
<span class=“delete” @click.stop=“deleteOne(item)”>
actions.js中:封装deleteSong()
export const deleteSong = function ({commit, state}, song){
let playlist = state.playlist.slice() //副本
let sequenceList = state.sequenceList.slice() //副本
let currentIndex = state.currentIndexlet pIndex = findIndex(playlist, song) playlist.splice(pIndex, 1) let sIndex = findIndex(sequenceList, song) sequenceList.splice(sIndex, 1) if(currentIndex > pIndex || currentIndex === playlist.length){ currentIndex-- } commit(types.SET_PLAYLIST, playlist) commit(types.SET_SEQUENCE_LIST, sequenceList) commit(types.SET_CURRENT_INDEX, currentIndex) const playingState = playlist.length > 0 commit(types.SET_PLAYING_STATE,playingState)
}
通过mapActions获取deleteSong方法
…mapActions([
‘deleteSong’
])
methods中定义deleteOne(),调用deleteSong()
deleteOne(item) {
this.deleteSong(item)
if(!this.playlist.length){
this.hide()
}
}
坑:报错this.currentSong.getLyric is not a function
原因: action deleteSong修改了playlist和currentIndex,导致currentSong发生变化;在player.vue中的watch会监测到currentSong变化了,但其实这里列表中已经没有歌曲了;newSong为空的Object,此时将newSong和oldSong进行对比,都会返回undefined,所以报错
解决:在watch currentSong()中判断如果没有newSong.id,直接返回,不执行任何操作
if(!newSong.id) {
return
}
优化:给删除歌曲添加动画
将-
替换为
transition-group的关键: 内容 - 必须要有 :key=“index”
.item
height: 40px
&.list-enter-active, &.list-leave-active
transition: all 0.1s
&.list-enter, &.list-leave-to
height: 0
清除歌曲播放列表
同搜索历史列表,在点击清除按钮后,需要先显示一个confirm弹窗,然后选择确认或取消
给清空按钮添加点击事件,显示弹窗:
<span class=“clear” @click=“showConfirm”>
showConfirm() {
this.$refs.confirm.show()
}
给弹窗监听confirm事件,同时封装action,在confirm()中调用
<confirm ref=“confirm” text=“是否清空播放列表” confirmBtnText=“清空”
@confirm=“confirmClear”>
confirmClear() {
this.deleteSongList()
this.hide()
}
…mapActions([
‘deleteSong’,
‘deleteSongList’
])
actions.js中:
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)
}
坑:点击取消时,歌曲播放列表也会被关闭
原因:在playlist.vue中在<div class=“playlist” @click=“hide”>内,点击confirm会事件冒泡,触发hide
解决:让confirm组件更加独立,阻止事件冒泡export const playerMixin = {
computed: {
iconMode(){
return this.mode === playMode.sequence ? ‘icon-sequence’ : this.mode ===
playMode.loop ? ‘icon-loop’ : ‘icon-random’
},
…mapGetters([
‘sequenceList’,
‘currentSong’,
‘playlist’,
‘mode’
])
},
methods: {
changeMode(){
const mode = (this.mode + 1) % 3
this.setPlayMode(mode)
let list = null
if(mode === playMode.random){
list = shuffle(this.sequenceList)
}else{
list = this.sequenceList
}
this.resetCurrentIndex(list)
this.setPlayList(list)
},
resetCurrentIndex(list){
let index = list.findIndex((item) => { //es6语法
return item.id === this.currentSong.id
})
this.setCurrentIndex(index)
},
…mapMutations({
setPlayingState: ‘SET_PLAYING_STATE’,
setCurrentIndex: ‘SET_CURRENT_INDEX’,
setPlayMode: ‘SET_PLAY_MODE’,
setPlayList: ‘SET_PLAYLIST’
})
}
}
player和playlist中:应用playerMixin,同时删掉共用部分
import {playerMixin} from ‘@/common/js/mixin’mixins:[playerMixin],
playlist.vue中:动态绑定class,监听点击事件
<i class=“icon” :class=“iconMode” @click=“changeMode”>
computed: {
modeText() {
return this.mode === playMode.sequence ? ‘顺序播放’ : this.mode === playMode.random ? ‘随机播放’ : ‘单曲循环’
}
}
四、添加歌曲到列表add-song组件实现
components->add-song目录下:创建add-song.vue
页面的显示隐藏:同playlist.vue
搜索框和搜索结果:与search.vue共有一些相同的逻辑,使用mixin复用
mixin.js中创建新的searchMixin对象
export const searchMixin = {
computed: {
…mapGetters([
‘searchHistory’
])
},
data() {
return {
query: ‘’
}
},
methods: {
blurInput() {
this.KaTeX parse error: Expected 'EOF', got '}' at position 29: …x.blur() }̲, saveSea…refs.searchBox.setQuery(query)
},
…mapActions([
‘saveSearchHistory’,
‘deleteSearchHistory’
])
}
}
search和add-song中:应用searchMixin,同时删掉共用部分
import {searchMixin} from ‘@/common/js/mixin’mixins:[searchMixin],
add-song.vue中:绑定数据,监听事件
<search-box ref=“searchBox” @query=“onQueryChange” placeholder=“搜索歌曲”>
<suggest :query=“query” :showSinger=“showSinger” @select=“selectSuggest” @listScroll=“blurInput”>
切换Tab组件
base->switches目录下:创建switches.vueplayHistory: []
②mutations-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.vue中:当歌曲ready后通过mapActions向Vuex中写入数据
ready() {
this.songReady = true
this.savePlayHistory(this.currentSong)
}
…mapActions([
‘savePlayHistory’
])
catch.js中:实现对本地缓存的操作
//将歌曲数据保存到本地缓存
export function savePlay(song) {
let songs = storage.get(PLAY_KEY, [])
insertArray(songs, song, (item) => {
return item.id === song.id
}, PLAY_MAX_LENGTH)
storage.set(PLAY_KEY, songs)
return songs
}
//从本地缓存中取出歌曲数据
export function loadPlay() {
return storage.get(PLAY_KEY, [])
}
state.js中:修复playHistory初始值为当前本地缓存中的数据
playHistory: loadPlay()
actions.js中:引入savePlay方法将数据同时存入Vuex和本地缓存
export const savePlayHistory = function({commit}, song){
commit(types.SET_PLAY_HISTORY, savePlay(song))
}
add-song.js中:通过mapGetters获取Vuex中的播放历史computed: {
…mapGetters([
‘playHistory’
])
}
add-song.js中:监听select事件,通过mapActionis从播放历史中选择歌曲插入播放列表
@select=“selectSong”
import Song from ‘@/common/js/song’selectSong(song, index) {
if(index !== 0) {
//从playHistory中获取到的song还是一个对象,需要实例化为Song类
this.insertSong(new Song(song))
}
},
…mapActions([
‘insertSong’
])
搜索历史列表:复用SearchList组件以及searchMixin中的数据和方法
<search-list @delete=“deleteSearchHistory” @select=“addQuery”
:searches=“searchHistory”>
优化:在search-list.vue中通过transition-group给删除列表时添加动画
关键:中的元素一定要有key值区分元素间的不同
优化:在添加歌曲到列表页面显示时,判断当前显示列表,对应scroll重新计算,确保高度正确
show() {
this.showFlag = true
setTimeout(() => {
if(this.currentIndex === 0){
this. KaTeX parse error: Expected 'EOF', got '}' at position 31: …efresh() }̲else{ …refs.searchList.refresh()
}
})
}
顶部提示框
base->top-list目录下:创建top-list.vue
add-song.vue中:应用top-list,添加slot
1首歌曲已经添加到播放队列
分别在selectSuggest()和selectSong()中调用showTip()显示提示框top-list.vue中:在show()内调用定时器,设置提示框显示2s自动关闭
坑:如果快速的调用show(),会有很多定时器存在
解决:每次show()时,在调用新的timer前就先清空前面的timer
props: {
delay: {
type: Number,
default: 2000
}
}show() {
this.showFlag = true
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.hide()
}, this.delay)
}
五、歌曲列表组件scroll组件能力的扩展
坑:添加歌曲到列表后,歌曲播放列表的滚动位置不对了
原因:playlist中歌曲列表项的添加了transition-group动画,即添加歌曲后歌曲播放列表的高度需要一个100ms的过程;而外层在watch到数据的变化后,20ms就重新计算了,计算的高度是不对的
解决:扩展scroll组件,添加props属性refreshDelay,让外部组件可以自定义重新计算的延迟时间;扩展后再在外部组件的data中定义refreshDelay:100,最后在中传入:refreshDelay=“refreshDelay”
refreshDelay: {
type: Number,
default: 20
}watch: {
data() { //监测data的变化
setTimeout(() => {
this.refresh()
}, this.refreshDelay)
}
}
同上:search-list组件中的列表项也添加了transition-group动画;在引用到的search组件和add-song组件中都需要传入:refreshDelay=“refreshDelay”;因为两个组件复用了searchMixin,所以在searchMixin中定义refreshDelay:100-
替换为