1. 成果展示
真实接口地址 本项目使用的是真实线上的网易云API
线上演示地址 目前只做了每日推荐(需登录)以及排行榜功能,点个star
吧大佬们!
项目GitHub地址 new
分支是Vue3+TypeScript
, master
分支是去年用Vue2
写的
页面功能简单分析(具体实现往下滑)
2. 处理接口数据
创建当前播放歌曲信息接口
// src/typings/index.ts
// 歌曲详情里歌手信息
type artist = {
id: number,
name: string,
}
// 歌曲详情里专辑信息
type album = {
id: number,
name: string,
picUrl: string
}
export interface ILyric {
time: number,
lyric: string,
uid: number
}
// 歌曲详情
export interface IMusicDetail {
name: string,
id: number,
artist: artist,
album: album
}
// 当前播放歌曲信息
export interface IMusicInfo extends IMusicDetail {
url: string,
ids?: number[], // 当前播放列表的所有歌曲的id
isVip: boolean, // 当前音乐是否需要VIP
lyric: ILyric[]
}
创建vuex来存储当前播放歌曲
如何不知道如何使用vue3+ts创建vuex的,请点击右侧观看。Vue3 + TypeScript创建Vuex
// src/store/modules/player.ts
import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators'
import store from '@/store'
import { IMusicInfo } from '@/typings'
import { SET_MUSIC_VOLUME, SET_PLAYING_MUSIC, SET_PLAYING_MUSIC_INDEX } from '../types'
@Module({ dynamic: true, store, namespaced: true, name: 'player' })
class Player extends VuexModule {
playingMusic = {}
playingMusicIndex = -1
musicVolume = 1
get music() {
return JSON.parse(localStorage.playingMusic)
}
get index() {
return parseInt(JSON.parse(localStorage.playingMusicIndex))
}
get volume() {
return JSON.parse(localStorage.musicVolume)
}
// 设置当前播放歌曲信息
@Mutation
SET_PLAYING_MUSIC(music: IMusicInfo) {
this.playingMusic = music
localStorage.setItem('playingMusic', JSON.stringify(music))
}
// 设置当前播放歌曲的索引
@Mutation
SET_PLAYING_MUSIC_INDEX(index: number) {
this.playingMusicIndex = index
localStorage.setItem('playingMusicIndex', JSON.stringify(index))
}
// 设置全局播放音量
@Mutation
SET_MUSIC_VOLUME(volume: number) {
this.playingMusicIndex = volume
localStorage.setItem('musicVolume', JSON.stringify(volume))
}
@Action({ rawError: true })
setPlayingMusic(music: IMusicInfo) {
this.context.commit(SET_PLAYING_MUSIC, music)
}
@Action({ rawError: true })
setPlayingMusicIndex(index: number) {
this.context.commit(SET_PLAYING_MUSIC_INDEX, index)
}
@Action({ rawError: true })
setMusicVolume(volume: number) {
this.context.commit(SET_MUSIC_VOLUME, volume)
}
}
export const PlayerModule = getModule(Player)
调用接口获取数据并处理
// src/utils/index.ts
// 根据id获取歌曲 并存到vuex
export const handleGetMusic = (id: string, ids?: number[]):Promise<object> => {
return new Promise((resolve, reject) => {
GetMusicDetail({ ids: id }).then(res => {
const detail = formatMusicDetail(res.songs)
GetMusicUrl({ id }).then(res2 => {
const url = res2.data[0].url
const isVip = res2.data[0].fee === 1
console.log('****************************************************************************************************************************************************')
console.log(url)
GetMusicLyrics({ id }).then(res => {
// 目前只处理原歌词(不处理翻译歌词)
const lyrics = formatMusicLyrics(res.lrc.lyric, res.tlyric.lyric)
const playingMusic = {
name: detail.name,
id: detail.id,
album: detail.album,
artist: detail.artist,
url,
ids,
isVip,
lyric: lyrics.lyric
}
PlayerModule.setPlayingMusic(playingMusic) // 设置当前播放歌曲
HistoryModule.setHistoryMusic(playingMusic) // 设置历史播放歌曲
resolve({ code: 200 })
}).catch(e => { reject(e) })
}).catch(e => { reject(e) })
}).catch(e => { reject(e) })
})
}
在页面中使用
建议放在播放页的前一个页面进行使用(也就是歌曲列表页)。获取到当前点击音乐的id和当前列表所有音乐的ids后,传入到handleGetMusic()
函数里,在then
回调方法里再做页面的跳转
或loading的处理
。
下面上一个示例:
const handleMusicItemClick = async(value: {songId: string, songIndex: string}) => {
loading.value = true
const songId = value.songId
const songIndex = Number(value.songIndex)
PlayerModule.setPlayingMusicIndex(songIndex)
const canplay = await MusicCanPlay({ id: songId })
if (canplay.success) { // 当前歌曲有版权
handleGetMusic(songId, detail.value.ids).then(res => {
loading.value = false
$router.push({ path: '/play', query: { id: songId }})
})
} else {
instance.ctx.$Toast.fail('抱歉,正在争取版权中...')
}
}
以上内容已经获取到了播放页面所需要的数据,接下来我们就可以对音乐进行一些控制了
3. 歌曲播放、暂停、专辑图片旋转的实现
3.1 数据、dom结构、CSS样式定义
专辑图片的dom结构
// src/Play/index.vue
// img里绑定的src是根据当前视窗高度来在加载不同尺寸的图片
<div :class="{'songPic': true , 'rotate': isPlaying, 'rotate rotatePause': !isPlaying}" ref="songPic">
<img :src="`${playingMusic.album.picUrl}?param=${clientHeight < 650 ? '150y150' : '200y200'}`" >
</div>
上一首 暂停 下一首 的dom结构
// src/Play/index.vue
<!-- 上一首 暂停 下一首 -->
<div class="control">
<svg-icon @click="handlePrevMusic" class="prev" iconClass='prev'/>
<svg-icon @click="handleClickPause" class="playing" iconClass='playing' v-if="isPlaying"/>
<svg-icon @click="handleClickPlay" class="pause" iconClass='pause' v-if="!isPlaying"/>
<svg-icon @click="handleNextMusic" class="next" iconClass='next'/>
</div>
// src/Play/index.vue
setup() {
const isPlaying = ref<boolean>(false) // 当前歌曲播放状态
let playingMusic = ref<any>({}) // 当前播放歌曲的信息,从vuex里获取
let ids: number[] = [] // 播放列表所有歌曲的id,用来传入handleGetMusic函数
let currentIndex:number = -1 // 当前播放歌曲的索引
// 将当前播放歌曲的信息给整出来
playingMusic = PlayerModule.music
ids = PlayerModule.music.ids
currentIndex = PlayerModule.index
return {
isPlaying,
playingMusic,
ids
}
}
// src/styles/index.scss
// 旋转专辑相关
.rotate img{
animation: RotateCricle 15s linear infinite;
}
.rotatePause img{
animation-play-state:paused;
-webkit-animation-play-state:paused; /* Safari 和 Chrome */
}
@keyframes RotateCricle{
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
3.2 TS控制
// src/Play/index.vue
// 播放歌曲
const handleClickPlay = (): void => {
console.log('播放')
isPlaying.value = true
audio.value.play()
}
// 暂停歌曲
const handleClickPause = (): void => {
console.log('暂停')
isPlaying.value = false
audio.value.pause()
}
4. 处理歌曲播放时间
4.1 dom结构的定义
// src/Play/index.vue
<!-- 进度条区域含时间 -->
<div class="progress">
<div class="currentTime">{{currentTimeStr}}</div>
<div class="progress-container">
<progress-bar
@progressClick="handleClickProgress"
@progressMove="handleMoveProgress"
@progressTouch='handleTouchProgress'
:width="percentage"
/>
</div>
<div class="allTime">{{durationStr}}</div>
</div>
<!-- audio标签 -->
<audio
:src="playingMusic.url"
ref="audio"
@canplay="handleGetDuration" // 在这个钩子函数拿到总的播放时长
@timeupdate="handleTimeUpdate" // 这个钩子是实时触发的
@ended="handleMusicEnded" // 播放完成的钩子
>
</audio>
4.2 TS控制
4.2.1 获取歌曲总时长、处理歌曲正在播放的时间
// src/Play/index.vue
setup() {
const durationStr = ref<string>('') // 歌曲总时长 ss:mm格式
const currentTimeStr = ref<string>('') // 歌曲正在播放的时间 ss:mm格式
const percentage = ref<string>('') // 传递给progressBar组件的进度条宽度百分比
// 获取歌曲总时长
const handleGetDuration = (e: any): void => {
duration = e.target.duration
durationStr.value = handleFormatDuration(duration)
}
// 处理歌曲播放进程
const handleTimeUpdate = (e:any): void => {
const { currentTime } = e.target
currentTimeStr.value = handleFormatDuration(currentTime)
}
return {
durationStr,
currentTimeStr,
handleGetDuration,
handleTimeUpdate
}
}
4.2.2 将时间(number)类型处理成ss:mm(string)的函数
// stc/utils/index.ts
// 格式化歌曲播放时间
export const handleFormatDuration = (duration: number):string => {
const mins = Math.floor(duration / 60) < 10 ? `0${Math.floor(duration / 60)}` : Math.floor(duration / 60)
const sec = Math.floor(duration % 60) < 10 ? `0${Math.floor(duration % 60)}` : Math.floor(duration % 60)
return `${mins}:${sec}`
}
5. 实现歌曲播放进度条跟随歌曲时间自行滑动
5.1 首先要实现以下前面提到的progressBar
组件
传递给父组件三个事件
progressClick
: 点击进度条时触发,参数是当前进度条长度所占的百分比progressMove
:拖拽进度条时触发,参数是当前进度条长度所占的百分比progressTouch
:拖拽进度条结束时触发,参数是当前进度条长度所占的百分比
接收五个参数
strokeWidth
:进度条的高度,默认为4px
trackColor
:进度条轨道的颜色,默认为#e5e5e5
color
:进度条的颜色,默认为#ffb3a7
dotWidth
:进度条圆点的大小,默认为12px
width
:进度条的宽度,默认为0
5.1.1 dom结构
// src/views/Play/components/ProgressBar.vue
<template>
<div
class='progress-box'
ref="progressRef"
@click="handleClickProgress"
@touchstart='handleTouchStart'
@touchmove='handleTouchMove'
@touchend='handleTouchEnd'
style="width: 100%;"
>
<div
class="track"
:style="{
backgroundColor: trackColor,
height: `${strokeWidth}px`,
marginTop: `${(20 - Number(strokeWidth)) / 2}px`
}"
>
</div>
<div
class="progress-bar"
:style="{
backgroundColor: color,
height: `${strokeWidth}px`,
width: `${percentage}%`,
marginTop: `${(20 - Number(strokeWidth)) / 2}px`
}"
>
</div>
<div
class="progress-dot"
:style="{
width: `${dotWidth}px`,
height: `${dotWidth}px`,
backgroundColor: color,
marginTop: `${(20 - Number(dotWidth)) / 2}px`,
left: `${Number(percentage) === 100 ? `${Number(percentage) - 1.5}%` : `${percentage}%`}`
}"
>
</div>
</div>
</template>
5.1.2 CSS样式
// src/views/Play/components/ProgressBar.vue
<style scoped lang="scss">
.progress-box{
position: relative;
height: 20px;
.track,
.progress-bar{
position: absolute;
left: 0;
top: 0;
border-radius: 4px;
}
.track{
width: 100%;
}
.progress-dot{
position: absolute;
left: 0;
top: 0;
border-radius: 50%;
}
}
</style>
5.1.3 TS控制
// src/views/Play/components/ProgressBar.vue
<script lang='ts'>
import { defineComponent, onMounted, PropType, ref, watch } from 'vue'
export default defineComponent({
name: 'ProgressBar',
props: {
strokeWidth: {
type: String as PropType<String>,
default: '4'
},
trackColor: {
type: String as PropType<String>,
default: '#e5e5e5'
},
color: {
type: String as PropType<String>,
default: '#ffb3a7'
},
width: {
type: String as PropType<String>,
default: '0'
},
dotWidth: {
type: String as PropType<String>,
default: '12'
}
},
setup(props, ctx) {
// const width = props.width
const percentage = ref<string>('') // 传过来的进度条宽度
const progressRef = ref<any>(null) // 整个progress组件 用来获取长度
const progressWidth = ref<number>(0) // progress的长度
let touchStart:number = 0
let touchEnd:number = 0
// 实时更新进度条的宽度
watch(() => props.width, (newValue, oldValue) => {
percentage.value = newValue as string
})
// 点击进度条事件
const handleClickProgress = (event: MouseEvent):void => {
const e = event || window.event
const position = e.clientX - progressRef.value.offsetLeft // 当前点击位置距离进度条最左边的距离
percentage.value = ((position / progressWidth.value) * 100).toFixed(3).toString()
ctx.emit('progressClick', percentage.value)
}
// 拖动进度条事件
const handleTouchStart = (event: TouchEvent):void => {
console.log(`拖拽起始位置: ${event.touches[0].clientX}`)
touchStart = event.touches[0].clientX
}
const handleTouchMove = (event: TouchEvent):void => {
console.log(`拖拽到了: ${event.touches[0].clientX}`)
let moveX = event.touches[0].clientX - progressRef.value.offsetLeft // progressRef.value.offsetLeft是进度条左边距浏览器左侧的距离 不变的
if (moveX >= progressWidth.value) moveX = progressWidth.value
if (moveX <= 0) moveX = 0
percentage.value = ((moveX / progressWidth.value) * 100).toFixed(3).toString()
// 将拖拽中的进度传递给父组件 例如用于调整音量
ctx.emit('progressMove', percentage.value)
}
const handleTouchEnd = (event: TouchEvent):void => {
console.log(`拖拽结束位置: ${event.changedTouches[0].clientX}`)
touchEnd = event.changedTouches[0].clientX
if (touchStart === touchEnd) {
// 点击事件也会触发touch事件,所以用这个条件判断可以在触发的时候什么都不做
console.log('这是click事件触发的touch事件')
} else {
// 拖拽事件结束,将当前拖拽进度传递给父组件
ctx.emit('progressTouch', percentage.value)
}
}
onMounted(() => {
// 将进度条组件的宽度赋值给变量
progressWidth.value = progressRef.value.offsetWidth
})
return {
handleClickProgress,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
percentage,
progressRef
}
}
})
</script>
5.2 进度条自己滚动
setup() {
let isTouching: boolean = false // 是否正在拖动歌曲进度条
// 处理歌曲播放进程
const handleTimeUpdate = (e:any): void => {
const { currentTime } = e.target
currentTimeStr.value = handleFormatDuration(currentTime)
if (!isTouching) {
// 这里添加判断的目的是:进度条拖拽时,歌曲依旧正常播放。如果不加判断,歌曲会实时更新,听起来就跟磁带卡碟一样
percentage.value = ((currentTime / duration) * 100).toFixed(3).toString()
}
}
// 进度条拖拽事件
const handleMoveProgress = (val: string): void => {
console.log(`当前拖拽到的进度: ${val}%`)
isTouching = true
}
return{
handleTimeUpdate,
handleMoveProgress
}
}
6 歌曲进度条点击跳转播放以及拖拽播放
就很简单,只需给width
赋个值、更新一下歌曲的时间就好了。因为前面已经把实现功能都写完了
// src/views/Play/index.vue
setup() {
// 进度条点击事件
const handleClickProgress = (val: string): void => {
percentage.value = val
// 更新歌曲时间
audio.value.currentTime = duration * (Number(val) / 100)
}
// 进度条拖拽结束事件
const handleTouchProgress = (val: string): void => {
// 更新歌曲时间
audio.value.currentTime = duration * (Number(val) / 100)
}
return {
handleClickProgress,
handleTouchProgress
}
}
7 音量控制
7.1 dom结构
// src/views/Play/index.vue
<!-- 音量控制 -->
<div class="volume">
<svg-icon iconClass='volume'/>
<div class="progress-container">
<progress-bar
@progressClick="handleChangeVolume"
@progressMove="handleChangeVolume"
:width="volumePercentage"
/>
</div>
</div>
7.2 TS控制
// src/views/Play/index.vue
setup() {
const volumePercentage = ref<string>('')
// 音量进度条的点击/拖拽事件
const handleChangeVolume = (val: string): void => {
const volume = parseInt(val) / 100 // 音量区间在0 - 1
audio.value.volume = volume
PlayerModule.setMusicVolume(volume) // 将音量存进vuex,进行全局保存
volumePercentage.value = val
}
return {
volumePercentage,
handleChangeVolume
}
}
8 实现上一首/下一首的跳转
需要注意的是获取id时的索引的边界值处理 跳转到新的一首歌时,需要自动播放,还需重置一些状态(在解析歌词时尤为重要) 直接上代码吧
// src/views/Play/index.vue
setup() {
// 上一首
const handlePrevMusic = (): void => {
currentIndex -= 1
if (currentIndex < 0) currentIndex = ids.length - 1
PlayerModule.setPlayingMusicIndex(currentIndex)
const id = ids[currentIndex].toString()
handleGetMusic(id, ids).then(res => {
console.log(`跳转到上一首歌 index: ${currentIndex}`)
playingMusic.artist.name = PlayerModule.music.artist.name
playingMusic.album.picUrl = PlayerModule.music.album.picUrl
playingMusic.name = PlayerModule.music.name
playingMusic.url = PlayerModule.music.url
playingMusic.lyric = PlayerModule.music.lyric
audio.value.autoplay = true
// 重置状态
handleResetMusic()
})
}
// 下一首
const handleNextMusic = (): void => {
currentIndex += 1
if (currentIndex > ids.length - 1) currentIndex = 0
PlayerModule.setPlayingMusicIndex(currentIndex)
const id = ids[currentIndex].toString()
handleGetMusic(id, ids).then(res => {
console.log(`跳转到下一首歌 index: ${currentIndex}, name: ${PlayerModule.music.name}`)
playingMusic.artist.name = PlayerModule.music.artist.name
playingMusic.album.picUrl = PlayerModule.music.album.picUrl
playingMusic.name = PlayerModule.music.name
playingMusic.url = PlayerModule.music.url
playingMusic.lyric = PlayerModule.music.lyric
// 重置状态
handleResetMusic()
})
}
// 重置歌曲状态 跳转之后
const handleResetMusic = (): void => {
audio.value.autoplay = true
audio.value.play()
isPlaying.value = true
}
return {
handlePrevMusic,
handleNextMusic,
handleResetMusic
}
}
有个困扰就是 跳转新的歌曲 所有的信息都要挨着重新赋值... 目前不知道什么原因,如果有大佬知道,请留言告知小弟。
9 总结
!!记得要在omMouted
生命周期里实现歌曲的自动播放和取出全局保存的音量值哦!!
onMounted(() => {
handleAutoPlay()
volumePercentage.value = (PlayerModule.volume * 100).toString()
audio.value.volume = PlayerModule.volume
})
写的很冗余,不过功能以及如何实现的都讲清楚了的,感谢你的耐心观看!
这也是我学习Vue3 + TypeScript
的一个练手小demo。另外可以给源码点个star吗!万分感激!源码地址
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!