1. 成果展示

真实接口地址 本项目使用的是真实线上的网易云API

线上演示地址 目前只做了每日推荐(需登录)以及排行榜功能,点个star吧大佬们!

项目GitHub地址 main分支是Vue3+TypeScriptvue2分支是去年用Vue2写的

页面功能简单分析(具体实现往下滑)

头部 - 路由跳转以及歌曲信息

旋转图片部分 - 用歌曲是否播放控制图片的旋转(添加css类名即可)

歌词部分 - 控制包裹歌词的divtransform: translateY(0px)属性。audio标签的timeupdate钩子函数可以与当前歌词时间进行匹配,进而让当前匹配到的歌词往上滑动。请移步具体实现文章

播放模式部分 - 生成随机的索引,在ids数组里匹配, 拿到id后进行请求资源,然后播放。

歌曲播放控制部分 - audio.play()是播放, audio.pause()是暂停。上一首,下一首的控制:在歌曲列表页面(播放页面的前一个页面),会拿到所有歌曲的id,并存进一个叫ids的数组,通过切换ids的索引来拿到当前歌曲的id , 即可实现歌曲切换,要注意边界值

音量控制以及播放进度控制 - 写一个progressBar组件,下面详细分析。

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[], // 当前播放列表的所有歌曲的idisVip: boolean, // 当前音乐是否需要VIPlyric: ILyric[]
}

创建vuex来存储当前播放歌曲

如何不知道如何使用vue3+ts创建vuex的,请点击右侧观看。Vue3 + TypeScript创建Vuex

// src/store/modules/player.tsimport { 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 = -1musicVolume = 1get music() {return JSON.parse(localStorage.playingMusic)}get index() {return parseInt(JSON.parse(localStorage.playingMusicIndex))}get volume() {return JSON.parse(localStorage.musicVolume)}// 设置当前播放歌曲信息@MutationSET_PLAYING_MUSIC(music: IMusicInfo) {this.playingMusic = musiclocalStorage.setItem('playingMusic', JSON.stringify(music))}// 设置当前播放歌曲的索引@MutationSET_PLAYING_MUSIC_INDEX(index: number) {this.playingMusicIndex = indexlocalStorage.setItem('playingMusicIndex', JSON.stringify(index))}// 设置全局播放音量@MutationSET_MUSIC_VOLUME(volume: number) {this.playingMusicIndex = volumelocalStorage.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].urlconst isVip = res2.data[0].fee === 1console.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 = trueconst songId = value.songIdconst 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'}`" alt="Album">
</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.vuesetup() {const isPlaying = ref<boolean>(false) // 当前歌曲播放状态let playingMusic = ref<any>({}) // 当前播放歌曲的信息,从vuex里获取let ids: number[] = [] // 播放列表所有歌曲的id,用来传入handleGetMusic函数let currentIndex:number = -1 // 当前播放歌曲的索引// 将当前播放歌曲的信息给整出来playingMusic = PlayerModule.musicids = PlayerModule.music.idscurrentIndex = PlayerModule.indexreturn {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 = trueaudio.value.play()
}
// 暂停歌曲
const handleClickPause = (): void => {console.log('暂停')isPlaying.value = falseaudio.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.vuesetup() {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.durationdurationStr.value = handleFormatDuration(duration)}// 处理歌曲播放进程const handleTimeUpdate = (e:any): void => {const { currentTime } = e.targetcurrentTimeStr.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组件

传递给父组件三个事件

  1. progressClick: 点击进度条时触发,参数是当前进度条长度所占的百分比
  2. progressMove:拖拽进度条时触发,参数是当前进度条长度所占的百分比
  3. progressTouch:拖拽进度条结束时触发,参数是当前进度条长度所占的百分比

接收五个参数

  1. strokeWidth:进度条的高度,默认为4px
  2. trackColor:进度条轨道的颜色,默认为#e5e5e5
  3. color:进度条的颜色,默认为#ffb3a7
  4. dotWidth:进度条圆点的大小,默认为12px
  5. width:进度条的宽度,默认为0

5.1.1 dom结构

// src/views/Play/components/ProgressBar.vue<template><divclass='progress-box'ref="progressRef"@click="handleClickProgress"@touchstart='handleTouchStart'@touchmove='handleTouchMove'@touchend='handleTouchEnd'style="width: 100%;"><divclass="track":style="{      backgroundColor: trackColor,
      height: `${strokeWidth}px`,
      marginTop: `${(20 - Number(strokeWidth)) / 2}px`
    }"></div><divclass="progress-bar":style="{      backgroundColor: color,
      height: `${strokeWidth}px`,
      width: `${percentage}%`,
      marginTop: `${(20 - Number(strokeWidth)) / 2}px`
    }"></div><divclass="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.widthconst percentage = ref<string>('') // 传过来的进度条宽度const progressRef = ref<any>(null) // 整个progress组件 用来获取长度const progressWidth = ref<number>(0) // progress的长度let touchStart:number = 0let touchEnd:number = 0// 实时更新进度条的宽度watch(() => props.width, (newValue, oldValue) => {percentage.value = newValue as string})// 点击进度条事件const handleClickProgress = (event: MouseEvent):void => {const e = event || window.eventconst 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.valueif (moveX <= 0) moveX = 0percentage.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].clientXif (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.targetcurrentTimeStr.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.vuesetup() {// 进度条点击事件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.vuesetup() {const volumePercentage = ref<string>('')// 音量进度条的点击/拖拽事件const handleChangeVolume = (val: string): void => {const volume = parseInt(val) / 100 // 音量区间在0 - 1audio.value.volume = volumePlayerModule.setMusicVolume(volume) // 将音量存进vuex,进行全局保存volumePercentage.value = val}return {volumePercentage,handleChangeVolume}
}

8 实现上一首/下一首的跳转

需要注意的是获取id时的索引的边界值处理
跳转到新的一首歌时,需要自动播放,还需重置一些状态(在解析歌词时尤为重要)
直接上代码吧

// src/views/Play/index.vuesetup() {// 上一首const handlePrevMusic = (): void => {currentIndex -= 1if (currentIndex < 0) currentIndex = ids.length - 1PlayerModule.setPlayingMusicIndex(currentIndex)const id = ids[currentIndex].toString()handleGetMusic(id, ids).then(res => {console.log(`跳转到上一首歌 index: ${currentIndex}`)playingMusic.artist.name = PlayerModule.music.artist.nameplayingMusic.album.picUrl = PlayerModule.music.album.picUrlplayingMusic.name = PlayerModule.music.nameplayingMusic.url = PlayerModule.music.urlplayingMusic.lyric = PlayerModule.music.lyricaudio.value.autoplay = true// 重置状态handleResetMusic()})}// 下一首const handleNextMusic = (): void => {currentIndex += 1if (currentIndex > ids.length - 1) currentIndex = 0PlayerModule.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.nameplayingMusic.album.picUrl = PlayerModule.music.album.picUrlplayingMusic.name = PlayerModule.music.nameplayingMusic.url = PlayerModule.music.urlplayingMusic.lyric = PlayerModule.music.lyric// 重置状态handleResetMusic()})}// 重置歌曲状态 跳转之后const handleResetMusic = (): void => {audio.value.autoplay = trueaudio.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吗!万分感激!源码地址

Vue3+TypeScript实现网易云音乐WebApp(播放界面:播放、暂停、音量控制、播放进度控制(点击/拖拽进度条)、上一首、下一首)相关推荐

  1. Vue3+TypeScript实现网易云音乐WebApp(解析歌词,并实现自行匹配滚动)

    前言 最终实现效果gif: 这篇文章实现了gif里的其他功能 1. 实现思路 解析歌词 拿到歌词数组 -> lyricArr = [{time: 0, lyric: '给我你的爱', uid: ...

  2. Vue3.0 + typescript 高仿网易云音乐 WebApp

    Vue3.0 + typescript 高仿网易云音乐 WebApp 前言 Vue3.0 的正式发布,让我心动不已,于是尝试用 vue3 实现一个完整的项目,整个项目全部使用了 composition ...

  3. Vue3+node.js网易云音乐实战项目(八)

    播放界面实现 1.准备工作 2.顶部布局 3.中部唱片部分布局 4.底部部分布局 最后一个页面还没写完,由于我要去比赛,所以暂时先写到这,等放假了再写 其他页面可以看我页面专栏 Vue3实战项目-网易 ...

  4. Vue3+node.js网易云音乐实战项目(五)

    推荐歌单详细页面顶部 1.推荐歌单详细页面 1.1.导航条和背景 1.2.头像和简介 1.3.头部完整代码 1.4.链接 实现效果 1.推荐歌单详细页面 1.1.导航条和背景 推荐歌单页面做好后,我们 ...

  5. Android初级,实现网易云音乐歌曲列表界面效果,播放界面效果,ListView,ViewPager方法详解

    初学Android初级,第一篇博客文章,如有错误,还望批评指正! 本文主要内容以网易云音乐歌曲列表界面效果代码,播放音乐界面效果代码为主,并将ListView和ViewPager作为实现界面滑动功能的 ...

  6. Vue3+node.js网易云音乐实战项目(三)

    页面 一.头部导航栏布局 二.轮播图的实现 三.请求网易的banner图 四 链接 一.头部导航栏布局 首先我们看最上面这里的布局,大致可分为三个模块,顶部左边,顶部中间,顶部右边 那么我们在comp ...

  7. 仿网易云音乐日推界面(监听AppBarLayout滑动+动态高斯模糊)

    首先来看下日推界面的效果: 网易云音乐日推界面的亮点就是在上拉的时候,banner页面逐渐模糊.字体透明度下降,最后左上角显示出"每日推荐"的字体. 这个界面用户会觉得很清晰.便捷 ...

  8. Vue3+node.js网易云音乐实战项目(七)

    底部播放按钮和播放功能的实现 1.底部播放组件 2.音乐播放与暂停 3.切换歌曲 其他页面可以看我页面专栏 Vue3实战项目-网易云APP . 大家觉得有些地方可以写的更好写法可以给我留言私信,我会去 ...

  9. Vue3+node.js网易云音乐实战项目(六)

    图标和播放列表实现 1.收藏.评论和分享图标 2.播放列表 其他页面可以看我页面专栏 Vue3实战项目-网易云APP . 如果文章对你有帮助请点一个赞或收藏 1.收藏.评论和分享图标 接下来,我们实现 ...

最新文章

  1. android studio下的NDK开发详解
  2. 华为鸿蒙系统是否上线,华为官方:鸿蒙系统2.0上线,手机能否搭载鸿蒙操作系统?...
  3. Spring Boot学习总结(21)——SpringBoot集成Redis等缓存以注解的方式优雅实现幂等,防千万次重复提交实例代码
  4. 配置rsync同步+inotify实时监控
  5. Apache创建虚拟目录绑定域名
  6. linux下解除端口防火墙,Linux下防火墙配置、端口的开启和关闭
  7. python 颜色大全
  8. python中merge函数_Python Merge函数原理及用法解析
  9. 计算机网络ap参数,酒店计算机网络(含无线AP)系统主要技术参数.docx
  10. IoT物联网嵌入式设备中30种常见传感器模块简介及原理讲解
  11. 针对2021.12.12北大附中信息学奥赛选拔比赛应对策略
  12. 航班信息的查询与检索Java,航班信息的查询与检索
  13. 2018纪中夏季信息学集训总结
  14. 库函数 qsort 的用法
  15. 基于Android的聊天系统
  16. 【jzoj4597】【现世斩】【平衡树】【set】
  17. ASP.NET:母版页与内容页
  18. java 并发: 原子类
  19. 加密后的数据如何进行模糊查询?
  20. kali------kali更新源

热门文章

  1. 每次USB共享网络后,网络编号自动+1后清零方法
  2. window下配置nginx 及虚拟主机
  3. 计算机图形学在线作业,电子科大16秋《计算机图形学》在线作业1答案
  4. 简明Hadoop配置(3)——windows下eclipse连接虚拟机
  5. 魅蓝Note2跑分 MT6753性能究竟如何
  6. 解决win10安装过程中“windows安装程序无法将windows配置为在此计算机的硬件上运行“的问题
  7. 低代码发展系列专访之三:低代码平台会成为企业数字化基础设施么?
  8. 家用计算机的辐射,家用电脑辐射多大
  9. Windows编程---使用C/C++语言创建一个窗口
  10. bzoj4826[hnoi2017]影魔