最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3+TypeScript实现网易云音乐WebApp(播放界面:播放、暂停、音量控制、播放进度控制(点击/拖拽进度条)、上一首、下一首)

    正文概述 掘金(awsl)   2021-03-12   921

    1. 成果展示

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

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

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

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

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

    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组件

    传递给父组件三个事件

    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>
      <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吗!万分感激!源码地址


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

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元