010_Vue音乐播放器(player.vue 播放器组件)

不满意之前的页面结构,所以我重构了一下,以home.vue作为父级组件,recommend组件、singer组件、rank组件和search组件

作为子路由自建都归类到tab组件中。

player.vue 组件,放置在home.vue组件下,这个组件一直存在,不过由于v-show="isShow"的关系,vuex中的playList没有数据,所以隐藏掉了,然后根据fullScreen数据,来判断是展示正常播放器还是迷你播放器。

<template>
  <div class="player" v-show="isShow" :style="fullScreenStyle">
    <!-- 正常播放器 -->
    <transition name="normal" appear mode="in-out">
      <div class="normalPlayer" v-show="fullScreen">
        <!-- 整背景图 -->
        <div class="bgImage">
          <img width="100%" height="100%" :src="currentSongInfo.img" />
        </div>
        <!-- 顶部 -->
        <div class="top">
          <div class="back">
            <i class="el-icon-arrow-down icon-back" @click="goBack"></i>
          </div>
          <h1 class="title">{{currentSongInfo.song_name}}</h1>
          <h2 class="subtitle">{{currentSongInfo.author_name}}</h2>
        </div>
        <!-- 中部 唱片 -->
        <div
          class="middle"
          @touchstart="middleTouchStart"
          @touchmove.prevent="middleTouchMove"
          @touchend="middleTouchEnd"
        >
          <!-- 旋转图片部分 -->
          <div class="middle-l" ref="middleL">
            <div class="cd-wrapper">
              <div class="cd">
                <img :style="imageRotateStop" class="image" v-lazy="currentSongInfo.img" />
              </div>
            </div>
          </div>
          <!-- 歌词滚动部分 -->
          <Scroll class="middle-r" ref="lyricList" :data="currentLyrics && currentLyrics.lines">
            <div class="lyric-wrapper">
              <div v-if="currentLyrics">
                <p
                  class="text"
                  :class="{'currentLine':currentLineNum == index}"
                  ref="lyricLine"
                  v-for="(line,index) in currentLyrics.lines"
                  :key="index"
                >{{line.txt}}</p>
              </div>
            </div>
          </Scroll>
        </div>
        <!-- 底部 -->
        <div class="bottom">
          <!-- 图像/歌词切换 -->
          <div class="dot-wrapper">
            <span class="dot" :class="{'active':currentShow=='img'}"></span>
            <span class="dot" :class="{'active':currentShow=='lyric'}"></span>
          </div>
          <!-- 播放时间及进度条 -->
          <div class="progress-wrapper">
            <span class="item item-l">{{TimeFormat(currentTime)}}</span>
            <div class="progress-bar-wrapper">
              <progress-bar :percent="percent" @percentChange="onProgressBarChange"></progress-bar>
            </div>
            <span class="item item-r">{{TimeFormat(currentSong.duration)}}</span>
          </div>
          <!-- 按钮 -->
          <ul class="operators">
            <!-- 播放模式 -->
            <li class="icon i-left">
              <!-- <i class="icon-sequence el-icon-refresh-left"></i> -->
              <i @click="modeChange" :class="iconMode"></i>
            </li>
            <!-- 上一首 -->
            <li class="icon i-left">
              <i @click="lastSong" class="icon-prev el-icon-d-arrow-left"></i>
            </li>
            <!-- 播放/暂停 -->
            <li class="icon i-center">
              <i ref="togglePlaying" @click="togglePlaying" class="el-icon-video-pause"></i>
            </li>
            <!-- 下一首 -->
            <li class="icon i-right">
              <i @click="nextSong" class="icon-next el-icon-d-arrow-right"></i>
            </li>
            <!-- 喜欢/取消喜欢 -->
            <li class="icon i-right">
              <i class="icon icon-not-facorite el-icon-star-off"></i>
            </li>
          </ul>
        </div>
      </div>
    </transition>

    <!-- 迷你播放器 -->
    <div class="miniPlayer" v-show="!fullScreen" @click="setFullScreen">
      <!-- 图片 -->
      <div class="icon">
        <img :style="imageRotateStop" v-lazy="currentSongInfo.img" />
      </div>
      <!-- 文本 -->
      <div class="text">
        <h2 class="name">{{currentSongInfo.song_name}}</h2>
        <p class="desc">{{currentSongInfo.author_name}}</p>
      </div>
      <!-- 按钮 -->
      <div class="mini-icon">
        <i ref="miniTogglePlaying" @click="togglePlaying" class="el-icon-video-pause"></i>
      </div>
      <!-- 播放列表 -->
      <div class="mini-icon">
        <i class="el-icon-tickets"></i>
      </div>
    </div>

    <audio
      ref="audio"
      :src="currentSongInfo.play_url"
      @canplay="ready"
      @timeupdate="updateTime"
      @error="error"
      @ended="end"
    ></audio>
  </div>
</template>

<script>
import { mapMutations, mapGetters, mapActions } from "vuex";

import { getKugouMusicPlay } from "../../api/kugouMusicPlay";

import progressBar from "../base/progress-bar"; //进度条组件

import Lyric from "lyric-parser"; //歌词滚动播放插件

import Scroll from "../base/scroll";

export default {
  created() {
    this.touch = {}; //因为这个touch数据对象不需要get和set,所以放在created里面定义即可实现操作同一份数据
  },
  data() {
    return {
      emptyList: {
        list: [],
        index: -1
      },
      currentSongInfo: "", //根据currentSong的hash数据,发送请求,得到的歌曲详细信息
      fullScreenStyle: "position:fixed;",
      imageRotateStop: "animation-play-state: paused;", //停止旋转的style
      songReady: false, //连续切换曲目时会出错,使用此变量标识歌曲资源是否请求回来
      currentTime: 0, //播放当前时间
      currentLyrics: null, //当前歌曲歌词信息
      currentLineNum: 0, //当前歌曲歌词所在行
      currentShow: "img" //当前是 图像还是歌词
    };
  },
  methods: {
    ...mapActions(["selectPlay"]),
    ...mapMutations({
      set_fullScreen: "set_fullScreen",
      set_playing: "set_playing",
      set_currentIndex: "set_currentIndex",
      set_mode: "set_mode",
      set_playList: "set_playList"
    }),
    noPlayer() {
      this.selectPlay({
        list: this.emptyList,
        index: this.emptyList.index
      });
      //以下设置无效
      // this.set_playing(false);
      // this.set_currentIndex(-1);
    },
    goBack() {
      //将vuex的fullScreen数据设为false
      this.set_fullScreen(false);
      // this.noPlayer();
    },
    setFullScreen(event) {
      //非点击迷你播放器的播放按钮或歌曲列表按钮
      if (event.target.nodeName !== "I") {
        //将vuex的fullScreen数据设为true
        this.set_fullScreen(true);
      }
    },

    //请求歌曲播放信息
    async _getKugouMusicPlay() {
      let info = await getKugouMusicPlay(this.currentSong.hash);
      if (info["SSA-CODE"]) {
        if (info["SSA-CODE"] != "") {
          //表示请求过于频繁,今天无法再播放歌曲
          //使用element组件
          this.$message({
            type: "error",
            message: "请求过于频繁,今天无法再播放歌曲了哦",
            offset: -4, //自顶部栏向下偏移量
            duration: 2000
          });
          this.noPlayer(); //设置空数据,变现为没有获取到歌单歌曲列表
          setTimeout(() => {
            this.$router.go(-1);
            return false;
          }, 2000);
        } else {
          this.currentSongInfo = info;
          this.currentLyrics = new Lyric(info.lyrics, this.handleLyric); //当前歌词信息
          if (this.playing) {
            this.currentLyrics.play();
          }
        }
      } else {
        this.currentSongInfo = info;
        this.currentLyrics = new Lyric(info.lyrics, this.handleLyric); //当前歌词信息
        if (this.playing) {
          this.currentLyrics.play();
        }
      }
    },

    //切换播放playing数据状态
    togglePlaying() {
      //歌曲资源未准备完成,return
      if(!this.songReady){
        return;
      }
      this.set_playing(!this.playing);
      if(this.currentLyrics){
        this.currentLyrics.togglePlay();
      }
    },

    //上一首
    lastSong() {
      //歌曲资源尚未请求成功,不切换歌曲
      if (!this.songReady) {
        return;
      }
      //改变下标index,获取歌曲currentSong,
      //重新设置 当前播放歌曲信息 currentSongInfo
      let index = this.currentIndex - 1;
      if (index <= 0) {
        //歌曲列表结尾
        index = this.currentIndex - 1; //重头开始
      }
      this.set_currentIndex(index);
      if (!this.playing) {
        //暂停时切换歌曲需要改变state的playing状态
        this.togglePlaying();
      }
      this.songReady = false; //请求完当前歌曲,重新将songReady置为false
    },

    //下一首
    nextSong() {
      //歌曲资源尚未请求成功,不切换歌曲
      if (!this.songReady) {
        return;
      }
      let index = this.currentIndex + 1;
      if (index == this.playList.length) {
        //歌曲列表结尾
        index = 0; //重头开始
      }
      this.set_currentIndex(index);
      if (!this.playing) {
        //暂停时切换歌曲需要改变state的playing状态
        this.togglePlaying();
      }
      this.songReady = false; //请求完当前歌曲,重新将songReady置为false
    },

    //歌曲资源请求成功可以播放
    ready() {
      this.songReady = true;
    },

    //歌曲请求失败,也置为true,避免切换歌曲不执行
    error() {
      this.songReady = true;
    },
    updateTime(event) {
      this.currentTime = event.target.currentTime; //audio当前播放的时间,可读写
    },

    //用于将时间戳转换成所需时间格式
    TimeFormat(timeStamp) {
      timeStamp = timeStamp | 0; // |0 向下取整
      let minute = (timeStamp / 60) | 0; //取余得到分钟
      let second = timeStamp % 60; //取余得秒
      if (minute < 10) {
        minute = "0" + minute;
      }
      if (second < 10) {
        second = "0" + second;
      }
      return minute + ":" + second;
    },

    //根据 拖放 进度条 传回的percent百分比值,修改歌曲的播放位置,通过audio的currentTIme可读写属性
    onProgressBarChange(percent) {
      this.$refs.audio.currentTime = this.currentSong.duration * percent;
      if (!this.playing) {
        //非播放状态,拖拽后播放,改变播放状态
        this.togglePlaying();
      }
      if(this.currentLyrics){
        this.currentLyrics.seek(this.currentSong.duration * percent*1000);//拖动进度,改变歌词位置
      }
    },

    //切换播放模式
    modeChange() {
      let mode = this.mode;
      mode++;
      mode > 2 ? (mode = 0) : mode;
      this.set_mode(mode);
      //切换播放列表
      let list = null;
      //顺序列表作为参数,使用洗牌函数得到随机播放列表
      if ((mode = 2)) {
        //随机播放
        list = this.shuffle(this.sequenceList);
      } else {
        //顺序播放、单曲循环
        list = this.playList;
      }
      this.resetCurrentIndex(list); //设置当前播放曲目索引,保证歌曲不会因为歌单的改变而切换
      this.set_playList(list); //将新的播放列表提交到state中
    },

    // 避免切换播放模式时候重新设置了currentSong,导致当前播放歌曲被切换
    resetCurrentIndex(list) {
      let index = list.findIndex(item => {
        return item.audio_id == this.currentSong.audio_id; //ES6函数,在新播放列表中根据歌曲audio_id找到当前曲目的新索引
      });
      this.set_currentIndex(index); //重新设置一遍index,这样保证查找到的歌曲是同一首,即currentSong不变
    },

    //洗牌函数的封装
    getRandom(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min);
    },
    shuffle(arr) {
      //不修改原数组
      let _arr = arr.slice();
      for (let i = 0; i < _arr.length; i++) {
        let j = this.getRandom(0, i);
        let t = _arr[i];
        _arr[i] = _arr[j];
        _arr[j] = t;
      }
      return _arr;
    },

    // 歌曲播放结束的end事件
    end() {
      //循环播放
      if (this.mode == 1) {
        this.loop();
      } else {
        //切换到下一首
        this.nextSong();
      }
    },

    //循环播放函数loop
    loop() {
      this.$refs.audio.currentTime = 0;
      this.$refs.audio.play();
      if(this.currentLyrics){
        this.currentLyrics.seek(0);//循环播放时,最后将歌词偏移到初始位置
      }
    },

    //lyric-parser 监听歌词改变的回调函数
    handleLyric(newLyricLine) {
      //每次歌词行改变,更新data的当前歌词行,在页面结构中,根据index等于当前行,显示高亮
      this.currentLineNum = newLyricLine.lineNum;
      //如果当前歌词行数超过5条,通过scroll滑动,实现歌词居中
      if (newLyricLine.lineNum > 5) {
        //大于5行,滚动到当前行数-5行的节点
        let lineEl = this.$refs.lyricLine[newLyricLine.lineNum - 5];
        this.$refs.lyricList.scrollToElement(lineEl, 1000);
      } else {
        //五行之内,滚动到顶部即可
        this.$refs.lyricList.scrollToElement(0, 0, 1000);
      }
    },

    //图像/歌词滑动切换的效果
    middleTouchStart(e) {
      this.touch.initiated = true; //初始化
      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;
      }
      const left = this.currentShow === "img" ? 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)`; //vue组件无法直接访问节点,使用$el
      this.$refs.lyricList.$el.style.transform = `transformDuration:300ms`;
      this.$refs.middleL.style.opacity = 1 - this.touch.percent;
      this.$refs.middleL.style.transform = `transformDuration:300ms`;
    },
    middleTouchEnd() {
      let offsetWidth;
      let opacity;      //从右向左滑
      if (this.currentShow == "img") {
        //滑动距离大于10%
        if (this.touch.percent > 0.1) {
          offsetWidth = -window.innerWidth;
          opacity= 0;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
          this.currentShow = "lyric"; //同时改变当前显示状态
        } else {
          offsetWidth = 0;
          opacity= 1;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
        }
      }
      //从左向右滑
      else {
        if (this.percent < 0.9) {
          offsetWidth = 0;
          opacity= 1;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
          this.currentShow = "img";
        } else {
          offsetWidth = -window.innerWidth;
          opacity= 0;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
        }
      }
      this.$refs.lyricList.$el.style.transform = `translate3d(${offsetWidth}px,0,0)`; //vue组件无法直接访问节点,使用$el
      this.touch.initiated = false
    }
  },
  computed: {
    ...mapGetters([
      "playing", //播放状态
      "fullScreen", //是否全屏
      "playList", //播放列表
      "currentSong", //当前歌曲
      "currentIndex", //当前索引
      "mode", //播放模式
      "sequenceList" //顺序列表
    ]),
    isShow() {
      if (this.playList) {
        if (this.playList.length) {
          //播放列表有歌曲,展开播放器,发起请求,请求点击歌曲的详细信息
          this._getKugouMusicPlay();
          return true;
        }
      } else {
        return false;
      }
    },
    percent() {
      return this.currentTime / this.currentSong.duration; //根据当前播放时间来计算进度条百分比
    },
    //播放状态样式
    iconMode() {
      //mode : 0 顺序播放  1 单曲循环  2 随机播放
      return this.mode === 0
        ? "el-icon-refresh-left"
        : this.mode === 1
        ? "el-icon-refresh"
        : "el-icon-connection";
    }
  },
  watch: {
    fullScreen: function(newVal) {
      newVal == true
        ? (this.fullScreenStyle = "top:0;")
        : (this.fullScreenStyle = "bottom:0;height:0;"); //播放器全屏时的样式
    },
    currentSong(newSong, oldSong) {
      //当playingList或者index改变导致调用此监听,若audio_id不变,即currentSong不变,则不执行播放操作
      if (newSong.audio_id == oldSong.audio_id) {
        return;
      }
      if(this.currentLyrics){
        this.currentLyrics.stop();//歌曲改变,将之前的歌词播放定时器停止
      }
      //监听到currentSong数据变化,异步自动播放歌曲
      this.$nextTick(() => {
        this.set_playing(true); //改变vuex中的播放状态
        this.$refs.audio.play(); //播放音乐
      });
    },
    currentSongInfo(newSong, oldSong) {
      if (newSong.audio_id == oldSong.audio_id) {
        return;
      }
      //监听到currentSongInfo数据变化,异步自动播放歌曲
      this.$nextTick(() => {
        this.set_playing(true); //改变vuex中的播放状态
        this.$refs.audio.play(); //播放音乐
      });
    },
    playing(newPlaying) {
      const audio = this.$refs.audio; //缓存audio对象
      if (newPlaying) {
        //播放
        //播放时显示暂停图标  el-icon-video-pause
        this.$refs.togglePlaying.classList = "el-icon-video-pause";
        this.$refs.miniTogglePlaying.classList = "el-icon-video-pause";
        this.imageRotateStop = ""; //图片旋转
        this.$refs.audio.play();
      } else {
        //暂停
        //暂停时显示播放图标   el-icon-video-play
        this.$refs.togglePlaying.classList = "el-icon-video-play";
        this.$refs.miniTogglePlaying.classList = "el-icon-video-play";
        this.imageRotateStop = "animation-play-state: paused;"; //图片暂停旋转
        this.$refs.audio.pause();
      }
    }
  },
  components: {
    progressBar,
    Scroll
  }
};
</script>

<style lang="less" scoped>

其中currentSongInfo——当前歌曲详细信息(包括play_url数据等),使用devServer发起后台请求(因为需要伪造cookie)

其中传入的hash值作为get请求的参数

在devServer的before函数内劫持对应请求,其中Cookie值为可在使用中自己手动抓取然后写死在请求中即可

            Cookie:"kg_mid=bbbd01eb4d89517f78a82335ab0aec58; kg_dfid=2B054t0bTXp10XkJEz1QDSOQ; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1572510353,1572868917,1572868957,1572869876"

其中用于标识歌曲的currentSong由vuex数据中的playList和index计算得出,方便切换上一首下一首歌曲(在点击歌单歌曲的时候已将playList和index数据提交到state)

 

图片旋转

  用css3动画 @keyframes里设置transform:rotate(); 控制动画暂停和运动可以用属性:animation-play-state:paused(暂停)|running(运动);

且只能写在同一个class或style内,所以将animation-play-state:paused 封入一个data属性中,以style对象的形式动态引用

 

连续切换歌曲时候报错

当audio抛出的canplay事件时表示歌曲请求成功,使用ready函数接收,改变能否播放的标识,切换歌曲时,以此标识来表示能否播放。

audio的属性

  1. audioTracks 返回可用的音轨列表(MultipleTrackList对象)
  2. autoplay 媒体加载后自动播放
  3. buffered 返回缓冲部件的时间范围(TimeRanges对象)
  4. controller 返回当前的媒体控制器(MediaController对象)
  5. controls 显示播控控件
  6. crossOrigin CORS设置
  7. currentSrc 返回当前媒体的URL
  8. currentTime 当前播放的时间,单位秒
  9. defaultMuted 缺省是否静音
  10. defaultPlaybackRate 播控的缺省倍速
  11. duration 返回媒体的播放总时长,单位秒
  12. ended 返回当前播放是否结束标志
  13. error 返回当前播放的错误状态
  14. initialTime 返回初始播放的位置
  15. loop 是否循环播放
  16. mediaGroup 当前音视频所属媒体组 (用来链接多个音视频标签)
  17. muted 是否静音
  18. networkState 返回当前网络状态
  19. paused 是否暂停
  20. playbackRate 播放的倍速
  21. played 当前播放部件已经播放的时间范围(TimeRanges对象)
  22. preload 页面加载时是否同时加载音视频
  23. readyState 返回当前的准备状态 {
  24. &emsp;&emsp;&emsp;&emsp;0: HAVE_NOTHING 没有准备就绪的状态
  25. &emsp;&emsp;&emsp;&emsp;1: HAVE_METADATA 关于音频就绪的元数据
  26. &emsp;&emsp;&emsp;&emsp;2: HAVE_CURRENT_DATA 当前可用,但下一帧不确定
  27. &emsp;&emsp;&emsp;&emsp;3: HAVE_FUTURE_DATA 当前和下一帧可用
  28. &emsp;&emsp;&emsp;&emsp;4: HAVE_ENOUGH_DATA 有足够的数据支持播放
  29. }
  30. seekable 返回当前可跳转部件的时间范围(TimeRanges对象)
  31. seeking 返回用户是否做了跳转操作
  32. src 当前音视频源的URL
  33. startOffsetTime 返回当前的时间偏移(Date对象)
  34. textTracks 返回可用的文本轨迹(TextTrackList对象)
  35. videoTracks 返回可用的视频轨迹(VideoTrackList对象)
  36. volume 音量值

 

 

歌曲播放时间及自定义进度条

audio标签带有timeUpdate 事件,会自动抛出当前播放时间的时间戳,在此使用updateTime函数来接收当前播放时间。

由于currentTime是时间戳,所以需要将其格式化  TimeFormat函数

然后歌曲的总时长为currentSong的duration属性,也是一个时间戳,调用TimeFormat函数即可

效果页

进度条组件 progress-bar.vue 

// 播放器进度条组件
<template>
  <!-- 最外层总的长条 -->
  <div class="progress-bar" ref="progressBar" @click="progressClick">
    <!-- 里层进度条 -->
    <div class="bar-inner">
      <div class="progress" ref="progress"></div>
      <div
        class="progress-btn-wrapper"
        @touchstart="progressTouchStart"
        @touchmove="progressTouchMove"
        @touchend="progressTouchEnd"
      >
        <!-- 按钮-拖动 -->
        <div class="progress-btn" ref="progressBtn"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    //进度条百分比,由播放歌曲的时间得出百分比
    percent: {
      type: Number,
      default: 0
    }
  },
  created() {
    //维护同一个touch对象,不同回调函数中共享数据
    this.touch = {};
  },
  watch: {
    percent(newPercent) {
      if (newPercent >= 0 && this.touch.initiated == false) {
        //非拖动时进度条随歌曲播放而进行
        const barWidth = this.$refs.progressBar.clientWidth - 16; //减去按钮的宽度
        const offsetWidth = newPercent * barWidth; //得到偏移的宽度

        this._offset(offsetWidth);
      }
    }
  },
  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 - 16,
        Math.max(0, this.touch.left + deltaX)
      ); // 0~this.touch.left+deltaX
      this._offset(offsetWidth);
    },
    progressTouchEnd() {
      this.touch.initiated = false;
      this.triggerPercent();
    },
    //点击进度条
    progressClick(e) {
        this._offset(e.offsetX);
        this.triggerPercent();
    },
    //设置偏移量
    _offset(offsetWidth) {
      this.$refs.progress.style.width = offsetWidth + "px"; //进度条进行偏移
      this.$refs.progressBtn.style.left = offsetWidth + "px"; //按钮进行偏移
    },
    //联动到歌曲播放进度改变
    triggerPercent() {
      const barWidth = this.$refs.progressBar.clientWidth - 16; //减去按钮的宽度
      const percent = this.$refs.progress.clientWidth / barWidth;
      this.$emit("percentChange", percent); //拖动完成时抛出 百分比改变的时间,联动到歌曲播放进度改变
    }
  }
};
</script>

<style lang="less" scoped>
.progress-bar {
  position: relative;
  height: 30px;
  top: 0.5rem;
  .bar-inner {
    position: absolute;
    width: 100%;
    top: 13px;
    height: 4px;
    background-color: rgba(0, 0, 0, 0.8);
    .progress {
      position: absolute;
      height: 100%;
      background-color: red;
    }
    .progress-btn-wrapper {
      position: absolute;
      //   left: -8px;
      left: 0;
      top: -13px;
      width: 30px;
      height: 30px;
      .progress-btn {
        position: relative;
        touch-action: 7px;
        top: 0.4rem;
        left: 7px;
        box-sizing: border-box;
        width: 16px;
        height: 16px;
        border: 3px solid red;
        border-radius: 50%;
        background-color: red;
      }
    }
  }
}
</style>

根据传入的 percent(百分比,由currentTime和duration计算而来)播放时间百分比

效果页

接下来通过一系列touch事件实现拖拽按钮调整播放位置的操作

首先 touchstart 事件 ,将 拖拽 标识变量initiated变量置为true,根据event对象的touches[0].pageX得到touchstart的起始位置 startX,存入this.touch对象,然后由已播放进度条的宽度得到按钮的偏移量 left。

在touchmove事件中,首先判断是否 拖拽初始化,未初始化则return,然后通过移动后的touches[0].pageX起始坐标减去记录的起始坐标startX得出移动偏移量deltaX,this.touch.left + deltaX 得出 拖动后的位置,然后跟 this.$refs.progressBar.clientWidth - 16 (进度条最大值)比较,小于它则取得出的拖动width,大于则取this.$refs.progressBar.clientWidth - 16(进度条末尾),然后将拖动的距离作为参数调用_offset函数,在函数中对 进度条 和 按钮的位置进行设置,实现偏移效果。

上面只是实现拖动,歌曲实际播放位置并没有改变,在touchend事件中,修改初始化值为false

调用triggerPercent函数,将进度条偏移的百分比作为参数抛出percent函数给父组件处理。根据进度百分比,设置audio的currentTime的值来进行歌曲播放位置的改变。

效果页

 

修改播放模式(顺序播放,单曲循环,随机播放)

mapGetters引入mode和sequenceList数据

给播放模式绑定样式和事件

洗牌函数

    /*洗牌函数的封装*/
    getRandom(min, max) {
      return Math.floor(Math.random() * (max - min + 1) + min);
    },
    shuffle(arr) {
      //不修改原数组
      let _arr = arr.slice();
      for (let i = 0; i < _arr.length; i++) {
        let j = this.getRandom(0, i);
        let t = _arr[i];
        _arr[i] = _arr[j];
        _arr[j] = t;
      }
      return _arr;
    }

至此,可以通过点击播放模式来切换歌单,且保持当前歌曲的播放状态。

 

根据audio派发的ended事件,对歌曲播放完毕后进行处理

 

点击进度条按钮的bug

当点击到进度条按钮本身时,由于获取的节点对象错误,导致进度条进度移动的错误

 

歌词的显示

将歌词部分(由currentSongInfo中取得)插入到Scroll组件中,以进行滚动,并根据currentShow来表示当前应当显示图片还是歌词部分。

其中,歌词部分使用Lyric函数进行处理(由cnpm install lyric-parser --save安装)

此时歌词即能随着歌曲滚动。

头像和歌词部分左右滑动切换

    //图像/歌词滑动切换的效果
    middleTouchStart(e) {
      this.touch.initiated = true; //初始化
      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;
      }
      const left = this.currentShow === "img" ? 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)`; //vue组件无法直接访问节点,使用$el
      this.$refs.lyricList.$el.style.transform = `transformDuration:300ms`;
      this.$refs.middleL.style.opacity = 1 - this.touch.percent;
      this.$refs.middleL.style.transform = `transformDuration:300ms`;
    },
    middleTouchEnd() {
      let offsetWidth;
      let opacity;      //从右向左滑
      if (this.currentShow == "img") {
        //滑动距离大于10%
        if (this.touch.percent > 0.1) {
          offsetWidth = -window.innerWidth;
          opacity= 0;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
          this.currentShow = "lyric"; //同时改变当前显示状态
        } else {
          offsetWidth = 0;
          opacity= 1;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
        }
      }
      //从左向右滑
      else {
        if (this.percent < 0.9) {
          offsetWidth = 0;
          opacity= 1;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
          this.currentShow = "img";
        } else {
          offsetWidth = -window.innerWidth;
          opacity= 0;
          this.$refs.middleL.style.opacity = opacity;
          this.$refs.middleL.style.transform = `transformDuration:300ms`;
        }
      }
      this.$refs.lyricList.$el.style.transform = `translate3d(${offsetWidth}px,0,0)`; //vue组件无法直接访问节点,使用$el
      this.touch.initiated = false
    }

样式

    .middle {
      width: 202vw;
      .middle-l {
        width: 100vw;
        vertical-align: top;
        display: inline-block;
        transition: all 1s;
        .cd-wrapper {
          .cd {
            .image {
              width: 17rem;
              border: 2px solid gray;
              border-radius: 50%;
              animation: imgRotate 20s linear infinite;
            }
          }
        }
      }
      .middle-r {
        width: 100vw;
        display: inline-block;
        transition: all 1s;
        .lyric-wrapper {
          .text {
            line-height: 2;
            color: rgba(255, 255, 255, 0.5);
            font-size: 1rem;
          }
          // 当前行歌词高亮
          .currentLine {
            font-size: 1.2rem;
            color: rgba(255, 255, 255, 1);
          }
        }
      }
      .wrapper {
        height: 63vh !important;
      }
    }

此时图像和歌词能够左右切换。

循环播放时歌词偏移回初始位置

歌曲播放/暂停时 ,歌词滚动状态的改变

拖动进度条,根据百分比,修改歌词的偏移位置

效果图

 

 

酷狗API歌曲数据请求有次数限制,当天次数限制时,弹出提示框后跳回歌单详情页。

效果页:

 

 

 

 

 

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值