Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第七章 播放器内置组件开发)
播放器Vuex数据设计
- state
1
2
3
4
5
6
7
8
9const state = {
singer: {},
playing: false,
fullScreen: false,
playlist: [],
sequenceList: [],
mode: playMode.sequence,
currentIndex: -1
}
state只保留最基础的数据, 计算属性放在getters中
getters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15export const playing = state => state.playing
export const fullScreen = state => state.fullScreen
export const playlist = state => state.playlist
export const sequenceList = state => state.sequenceList
export const mode = state => state.mode
export const currentIndex = state => state.currentIndex
export const currentSong = (state) => {
return state.playlist[state.currentIndex] || {}
}mutation-types
1
2
3
4
5
6
7
8
9
10
11export const SET_PLAYING_STATE = 'SET_PLAYING_STATE'
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'
export const SET_PLAYLIST = 'SET_PLAYLIST'
export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'
export const SET_PLAY_MODE = 'SET_PLAY_MODE'
export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'mutations
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
27import * as types from './mutation-types'
const matutaions = {
[types.SET_SINGER](state, singer) {
state.singer = singer
},
[types.SET_PLAYING_STATE](state, flag) {
state.playing = flag
},
[types.SET_FULL_SCREEN](state, flag) {
state.fullScreen = flag
},
[types.SET_PLAYLIST](state, list) {
state.playlist = list
},
[types.SET_SEQUENCE_LIST](state, list) {
state.sequenceList = list
},
[types.SET_PLAY_MODE](state, mode) {
state.mode = mode
},
[types.SET_CURRENT_INDEX](state, index) {
state.currentIndex = index
}
}
export default matutaions
actions的操作通常有两种
- 异步操作
- 对mutation的封装
播放器Vuex的相关应用
新建组件player.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div class="player">
<div class="normal-player">
播放器
</div>
<div class="mini-player"></div>
</div>
</template>
<script>
export default {}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
</style>在App.vue中注册组件
在song-list.vue 中派发事件
1
2
3selectItem(song, index) {
this.$emit('select', item, index)
}在actions定义动作
1
2
3
4
5
6
7
8
9import * as types from './mutation-types'
export const selectPlay = function ({commit, state}, {list, index}) {
commit(types.SET_SEQUENCE_LIST, list)
commit(types.SET_PLAYLIST, randomList)
commit(types.SET_CURRENT_INDEX, index)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
}在music-list.vue 接收事件
1
2
3
4
5
6
7
8
9selectItem(item, index) {
this.selectPlay({
list: this.songs,
index
})
},
...mapActions([
'selectPlay'
])
播放器基础样式及歌曲数据的应用
1 | <template> |
播放器展开收起动画
第三方库: create-keyframe-animation1
2
3
4
5
6
7
8<transition name="normal"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave">
...normal
</transition>
<script>
1 | // 导入插件 |
播放器歌曲播放功能实现
添加radio
1
<audio ref="audio" :src="currentSong.url"></audio>
添加methods: togglePlaying, watch: currentSong,
playing1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// methods
togglePlaying() {
this.setPlayingState(!this.playing)
}
// watch
currentSong(newSong, oldSong) {
this.$nextTick(() => {
this.$refs.audio.play()
})
},
playing(newPlaying) {
const audio = this.$refs.audio
this.$nextTick(() => {
newPlaying ? audio.play() : audio.pause()
})
}添加计算属性, 计算图标
1
2
3
4
5
6playIcon() {
return this.playing ? 'icon-pause' : 'icon-play'
},
miniIcon() {
return this.playing ? 'icon-pause-mini' : 'icon-play-mini'
}添加计算属性, 计算图标是否旋转
1
2
3cdCls() {
return this.playing ? 'play' : 'play pause'
}
1 | .cd { |
播放器歌曲前进后退功能实现
- 添加歌曲上一首, 下一首事件
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// 播放下一首歌
next() {
if (!this.songReady) {
return
}
let index = this.currentIndex + 1
if (index === this.playlist.length) {
index = 0
}
this.setCurrentIndex(index)
if (!this.playing) {
this.togglePlaying()
}
this.songReady = false
},
// 播放上一首歌
prev() {
if (!this.songReady) {
return
}
let index = this.currentIndex - 1
if (index === -1) {
index = this.playlist.length - 1
}
this.setCurrentIndex(index)
if (!this.playing) {
this.togglePlaying()
}
this.songReady = false
},
ready() {
this.songReady = true
}
播放器播放时间获取和更新
添加事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 获取播放时间
updateTime(e) {
this.currentTime = e.target.currentTime
}
// 补位
_pad(num, n = 2) {
let len = num.toString().length
while (len < n) {
num = '0' + num
len++
}
return num
},
// 格式化
format(interval) {
interval = interval | 0
const minute = interval / 60 | 0
const second = this._pad(interval % 60)
return `${minute}:${second}`
}
播放器progress-bar进度条组件实现
palyer.vue 添加计算属性
1
2
3percent() {
return this.currentTime / this.currentSong.duration
}添加基础组件progress-bar.vue
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<template>
<div class="progress-bar" ref="progressBar" @click="progressClick">
<div class="bar-inner">
<div class="progress" ref="progress"></div>
<div class="progress-btn-wrapper" ref="progressBtn"
@touchstart.prevent="progressTouchStart"
@touchmove.prevent="progressTouchMove"
@touchend="progressTouchEnd"
>
<div class="progress-btn"></div>
</div>
</div>
</div>
</template>
<script>
import {prefixStyle} from 'common/js/dom'
const progressBtnWidth = 16
const transform = prefixStyle('transform')
export default {
props: {
percent: {
type: Number,
default: 0
}
},
created() {
this.touch = {}
},
methods: {
progressTouchStart(e) {
// 标识位, 表示已经初始化
this.touch.initiated = true
// 点击位置
this.touch.startX = e.touches[0].pageX
// 进度条宽度
this.touch.left = this.$refs.progress.clientWidth
},
progressTouchMove(e) {
if (!this.touch.initiated) {
return
}
// 偏移量
const deltaX = e.touches[0].pageX - this.touch.startX
// 移动距离
const offsetWidth = Math.min(this.$refs.progressBar.clientWidth - progressBtnWidth, Math.max(0, this.touch.left + deltaX))
this._offset(offsetWidth)
},
progressTouchEnd() {
this.touch.initiated = false
this._triggerPercent()
},
/**
* 点击进度条事件
* @param {Object} e event对象
*/
progressClick(e) {
const rect = this.$refs.progressBar.getBoundingClientRect()
const offsetWidth = e.pageX - rect.left
this._offset(offsetWidth)
// 这里当我们点击 progressBtn 的时候,e.offsetX 获取不对
// this._offset(e.offsetX)
this._triggerPercent()
},
_triggerPercent() {
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
const percent = this.$refs.progress.clientWidth / barWidth
this.$emit('percentChange', percent)
},
_offset(offsetWidth) {
this.$refs.progress.style.width = `${offsetWidth}px`
this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px,0,0)`
}
},
watch: {
percent(newPercent) {
// 没有在拖动中
if (newPercent >= 0 && !this.touch.initiated) {
// 进度条宽度
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
// 偏移宽度
const offsetWidth = newPercent * barWidth
this._offset(offsetWidth)
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.progress-bar
height: 30px
.bar-inner
position: relative
top: 13px
height: 4px
background: rgba(0, 0, 0, 0.3)
.progress
position: absolute
height: 100%
background: $color-theme
.progress-btn-wrapper
position: absolute
left: -8px
top: -13px
width: 30px
height: 30px
.progress-btn
position: relative
top: 7px
left: 7px
box-sizing: border-box
width: 16px
height: 16px
border: 3px solid $color-text
border-radius: 50%
background: $color-theme
</style>组件player.vue 添加事件
1
2
3
4
5
6
7onProgressBarChange(percent) {
const currentTime = this.currentSong.duration * percent
this.$refs.audio.currentTime = currentTime
if (!this.playing) {
this.togglePlaying()
}
},
播放器progress-circle 圆形进度条组件实现
添加基础组件progress-circle1
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<template>
<div class="progress-circle">
<!-- viewBox和r成比例 -->
<svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
<circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray"
:stroke-dashoffset="dashOffset"/>
</svg>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
radius: {
type: Number,
default: 100
},
percent: {
type: Number,
default: 0
}
},
data() {
return {
dashArray: Math.PI * 100
}
},
computed: {
dashOffset() {
return (1 - this.percent) * this.dashArray
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "~common/stylus/variable"
.progress-circle
position: relative
circle
stroke-width: 8px
transform-origin: center
&.progress-background
transform: scale(0.9)
stroke: $color-theme-d
&.progress-bar
transform: scale(0.9) rotate(-90deg)
stroke: $color-theme
</style>
播放器模式切换功能实现
添加common/js/util.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
export function shuffle(arr) {
let _arr = arr.slice()
for (let i = 0; i < _arr.length; i++) {
let j = getRandomInt(0, i)
let t = _arr[i]
_arr[i] = _arr[j]
_arr[j] = t
}
return _arr
}在play.vue中添加方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18changeMode() {
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)
},在player.vue中添加方法
1
2
3
4
5
6
7
8
9
10
11end() {
if (this.mode === playMode.loop) {
this.loop()
} else {
this.next()
}
},
loop() {
this.$refs.audio.currentTime = 0
this.$refs.audio.play()
},在store/action中添加乱序播放方法
1
2
3
4
5
6
7
8
9export const randomPlay = function ({commit}, {list}) {
commit(types.SET_PLAY_MODE, playMode.random)
commit(types.SET_SEQUENCE_LIST, list)
let randomList = shuffle(list)
commit(types.SET_PLAYLIST, randomList)
commit(types.SET_CURRENT_INDEX, 0)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
}在music-list.vue中添加方法
1
2
3
4
5random() {
this.randomPlay({
list: this.songs
})
},修改store/action
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function findIndex(list, song) {
return list.findIndex((item) => {
return item.id === song.id
})
}
export const selectPlay = function({ commit, state }, { list, index }) {
commit(types.SET_SEQUENCE_LIST, list)
if (state.mode === playMode.random) {
let randomList = shuffle(list)
commit(types.SET_PLAYLIST, randomList)
index = findIndex(randomList, list[index])
} else {
commit(types.SET_PLAYLIST, list)
}
commit(types.SET_CURRENT_INDEX, index)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
}
播放器歌词数据抓取
添加api/song.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import {commonParams} from './config'
import axios from 'axios'
export function getLyric(mid) {
const url = '/api/lyric'
const data = Object.assign({}, commonParams, {
songmid: mid,
platform: 'yqq',
hostUin: 0,
needNewCode: 0,
categoryId: 10000000,
pcachetime: +new Date(),
format: 'json'
})
return axios.get(url, {
params: data
}).then((res) => {
return Promise.resolve(res.data)
})
}build/dev-server.js添加方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23apiRoutes.get('/lyric', function (req, res) {
var url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg'
axios.get(url, {
headers: {
referer: 'https://c.y.qq.com/',
host: 'c.y.qq.com'
},
params: req.query
}).then((response) => {
var ret = response.data
if (typeof ret === 'string') {
var reg = /^\w+\(({[^()]+})\)$/
var matches = ret.match(reg)
if (matches) {
ret = JSON.parse(matches[1])
}
}
res.json(ret)
}).catch((e) => {
console.log(e)
})
})song.js class 添加获取歌词方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16getLyric() {
if (this.lyric) {
return Promise.resolve(this.lyric)
}
return new Promise((resolve, reject) => {
getLyric(this.mid).then((res) => {
if (res.retcode === ERR_OK) {
this.lyric = Base64.decode(res.lyric)
resolve(this.lyric)
} else {
reject('no lyric')
}
})
})
}
播放器歌词滚动列表实现
1 | handleLyric({lineNum, txt}) { |
播放器歌词左右滑动实现
created时添加变量touch
1
2
3created() {
this.touch = {}
},添加滑动事件
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
62middleTouchStart(e) {
this.touch.initiated = true
// 用来判断是否是一次移动
this.touch.moved = false
const touch = e.touches[0]
this.touch.startX = touch.pageX
this.touch.startY = touch.pageY
},
middleTouchMove(e) {
if (!this.touch.initiated) {
return
}
const touch = e.touches[0]
const deltaX = touch.pageX - this.touch.startX
const deltaY = touch.pageY - this.touch.startY
if (Math.abs(deltaY) > Math.abs(deltaX)) {
return
}
if (!this.touch.moved) {
this.touch.moved = true
}
const left = this.currentShow === 'cd' ? 0 : -window.innerWidth
const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX))
this.touch.percent = Math.abs(offsetWidth / window.innerWidth)
this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`
this.$refs.lyricList.$el.style[transitionDuration] = 0
this.$refs.middleL.style.opacity = 1 - this.touch.percent
this.$refs.middleL.style[transitionDuration] = 0
},
middleTouchEnd() {
if (!this.touch.moved) {
return
}
let offsetWidth
let opacity
if (this.currentShow === 'cd') {
// 从右向左滑, 超过10%, 滚动屏幕
if (this.touch.percent > 0.1) {
offsetWidth = -window.innerWidth
opacity = 0
this.currentShow = 'lyric'
} else {
offsetWidth = 0
opacity = 1
}
} else {
if (this.touch.percent < 0.9) {
offsetWidth = 0
this.currentShow = 'cd'
opacity = 1
} else {
offsetWidth = -window.innerWidth
opacity = 0
}
}
const time = 300
this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)`
this.$refs.lyricList.$el.style[transitionDuration] = `${time}ms`
this.$refs.middleL.style.opacity = opacity
this.$refs.middleL.style[transitionDuration] = `${time}ms`
this.touch.initiated = false
},
播放器歌词剩余功能实现
播放器底部播放器适配+mixin的应用
添加common/js/mixin.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import {mapGetters} from 'vuex'
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')
}
}
}
1 | // 引入mixin |