Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第11章 歌曲列表组件)

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第11章 歌曲列表组件)

歌曲列表组件布局和功能介绍

添加playlist组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<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="icon-clear"></i></span>
</h1>
</div>
<scroll ref="listContent" class="list-content">
<transition-group name="list" tag="ul">
<li ref="listItem" class="item">
<i class="current"></i>
<span class="text"></span>
<span class="like">
<i class="icon-not-favorite"></i>
</span>
<span class="delete">
<i class="icon-delete"></i>
</span>
</li>
</transition-group>
</scroll>
<div class="list-operate">
<div class="add">
<i class="icon-add"></i>
<span class="text">添加歌曲到队列</span>
</div>
</div>
<div class="list-close">
<span>关闭</span>
</div>
</div>
</div>
</transition>
</template>

<script>
export default {
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
@import "~common/stylus/mixin"
.playlist
position: fixed
left: 0
right: 0
top: 0
bottom: 0
z-index: 200
background-color: $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-fade-enter
.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
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
&.list-enter-active, &.list-leave-active
transition: all 0.1s
&.list-enter, &.list-leave-to
height: 0
.current
flex: 0 0 20px
width: 20px
font-size: $font-size-small
color: $color-theme-d
.text
flex: 1
no-wrap()
font-size: $font-size-medium
color: $color-text-d
.like
extend-click()
margin-right: 15px
font-size: $font-size-small
color: $color-theme
.icon-favorite
color: $color-sub-theme
.delete
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-right: 5px
font-size: $font-size-small-s
.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>

歌曲列表组件显示和隐藏的控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 蒙层添加点击关闭事件 -->
<div class="playlist" v-show="showFlag" @click="hide">
<!-- 列表组件添加阻止冒泡事件 -->
<div class="list-wrapper" @click.stop>
...
</div>
</div>

<script>
export default {
data() {
return {
showFlag: false
}
},
methods: {
show() {
this.showFlag = true
},
hide() {
this.showFlag = false
}
}
}
</script>

歌曲列表组件播放列表的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
export default {
data() {
return {
showFlag: false
}
},
computed: {
...mapGetters([
'sequenceList',
'currentSong',
'playlist',
'mode'
])
},
methods: {
show() {
this.showFlag = true
setTimeout(() => {
this.$refs.listContent.refresh()
this.scrollToCurrent(this.currentSong)
}, 20)
},
hide() {
this.showFlag = false
},
getCurrentIcon(item) {
return this.currentSong.id === item.id ? 'icon-play' : ''
},
selectItem(item, index) {
// 播放模式是随机播放, 找到歌曲索引
if (this.mode === playMode.random) {
index = this.playlist.findIndex((song) => {
return song.id === item.id
})
}
this.setCurrentIndex(index)
this.setPlayingState(true)
},
scrollToCurrent(current) {
const index = this.sequenceList.findIndex((song) => {
return current.id === song.id
})
this.$refs.listContent.scrollToElement(this.$refs.listItem[index], 300)
},
...mapMutations({
'setCurrentIndex': 'SET_CURRENT_INDEX',
'setPlayingState': 'SET_PLAYING_STATE'
})
},
watch: {
currentSong(newSong, oldSong) {
if (!this.showFlag || newSong.id === oldSong.id) {
return
}
this.scrollToCurrent(newSong)
}
},
components: {
Scroll
}
}

添加action, 清空playlist

1
2
3
4
5
6
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)
}

歌曲列表组件 playerMixin的抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
export const playlistMixin = {
computed: {
...mapGetters([
'playlist'
])
},
mounted() {
this.handlePlaylist(this.playlist)
},
activated() {
this.handlePlaylist(this.playlist)
},
watch: {
playlist(newVal) {
this.handlePlaylist(newVal)
}
},
methods: {
handlePlaylist() {
throw new Error('component must implement handlePlaylist method')
}
}
}

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) => {
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'
})
}
}

好处

  1. 节省代码量
  2. 方便代码维护

歌曲列表组件add-song组件实现

组件add-song简单dom结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<transition name="slide">
<div class="add-song">
<!-- 添加歌曲header start -->
<div class="header">
<h1 class="title">添加歌曲到列表</h1>
<div class="close">
<i class="icon-close"></i>
</div>
</div>
<!-- 添加歌曲header end -->
<!-- 搜索框 start -->
<div class="search-box-wrapper">
</div>
<!-- 搜索框 end -->
<!-- 快速导航 start -->
<div class="shortcut">
</div>
<!-- 快速导航 end -->
<!-- 搜索结果 start -->
<div class="search-result">
</div>
<!-- 搜索结果 end -->
</div>
</transition>
</template>

添加mixin, search和add-song组件公用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export const searchMixin = {
data() {
return {
query: '',
refreshDelay: 120
}
},
computed: {
...mapGetters([
'searchHistory'
])
},
methods: {
blurInput() {
this.$refs.searchBox.blur()
},
saveSearch() {
this.saveSearchHistory(this.query)
},
onQueryChange(query) {
this.query = query
},
addQuery(query) {
this.$refs.searchBox.setQuery(query)
},
...mapActions([
'saveSearchHistory',
'deleteSearchHistory'
])
}
}

添加基础组件switches

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<ul class="switches">
<li class="switch-item" v-for="(item,index) in switches" :class="{'active':currentIndex === index}"
@click="switchItem(index)">
<span>{{item.name}} </span>
</li>
</ul>
</template>

<script>
export default {
props: {
switches: {
type: Array,
default: []
},
currentIndex: {
type: Number,
default: 0
}
},
methods: {
switchItem(index) {
this.$emit('switch', index)
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.switches
display: flex
align-items: center
width: 240px
margin: 0 auto
border: 1px solid $color-highlight-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>

实现最近播放

1
2
3
4
5
<scroll class="list-scroll" v-if="currentIndex===0" :data="playHistory">
<div class="list-inner">
<song-list :songs="playHistory" @select="selectSong"></song-list>
</div>
</scroll>

实现搜索历史

1
2
3
4
5
<scroll ref="searchList" class="list-scroll" v-if="currentIndex===1" :data="searchHistory">
<div class="list-inner">
<search-list :searches="searchHistory" @delete="deleteSearchHistory" @select="addQuery"></search-list>
</div>
</scroll>

歌曲列表组件top-list组件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<transition name="drop">
<div class="top-tip" v-show="showFlag" @click.stop="hide">
<slot></slot>
</div>
</transition>
</template>

<script>
export default {
props: {
delay: {
type: Number,
default: 2000
}
},
data() {
return {
showFlag: false
}
},
methods: {
show() {
this.showFlag = true
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.hide()
}, this.delay)
},
hide() {
this.showFlag = false
}
}
}
</script>

<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.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>