年轻人的第一个Vue在线音乐播放器

4,052 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

Preface

前几天听五条人的歌,女和声+乐队的组合就老让我有种既视感,想起了高三的时候天天听的歌。

决定自己写一个全平台都能听的在线播放器,也省的下载了。

东拼西凑整了一个还算舒服的播放器,用到了Vue2.6,有二百多首歌,随机播放,就是电台的感觉,希望你们喜欢👀

在线试听:Github.ioCodepen

源码可以看这里:Github

image-20210922150845564

HTML

整体框架

🤡

<div class="wrapper" id="app">
  <div class="player">
    <div class="player__top">
      <!-- 封面,暂停/播放,上一首,下一首,❤,🔗 五个按钮-->
    </div>
    
    <div class="progress" ref="progress">
      <!-- 进度条,歌曲信息,当前播放时长,总时长-->
    </div>
  </div>
</div>

按钮都是用SVG画的,这里把 path 都省略了。

网上都有,直接copy源码吧 Github

<svg xmlns="http://www.w3.org/2000/svg" hidden xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
      
    <!-- 空心和实心的❤ -->
    <symbol id="icon-heart-o" viewBox="0 0 32 32">
      <title>icon-heart-o</title>
    </symbol>
    <symbol id="icon-heart" viewBox="0 0 32 32">
      <title>icon-heart</title>
    </symbol>
      
    <!-- 暂停和播放按钮 -->
    <symbol id="icon-pause" viewBox="0 0 32 32">
      <title>icon-pause</title>
    </symbol>
    <symbol id="icon-play" viewBox="0 0 32 32">
      <title>icon-play</title>
    </symbol>
      
    <!-- 链接按钮🔗 -->
    <symbol id="icon-link" viewBox="0 0 32 32">
      <title>link</title>
    </symbol>
      
      
    <!-- 上一首和下一首 -->
    <symbol id="icon-next" viewBox="0 0 32 32">
      <title>next</title>
    </symbol>
    <symbol id="icon-prev" viewBox="0 0 32 32">
      <title>prev</title>
    </symbol>
      
  </defs>
</svg>

专辑封面

<div class="player-cover">
    <transition-group :name="transitionName">
        <div class="player-cover__item" v-if="$index === currentTrackIndex" 
             :style="{ backgroundImage: `url(${track.cover})` }" 
             v-for="(track, $index) in tracks" :key="$index">
        </div>
    </transition-group>
</div>

播放 & 暂停

<div class="player-controls__item -xl js-play" @click="play">
    <svg class="icon">
        <use xlink:href="#icon-pause" v-if="isTimerPlaying"></use>
        <use xlink:href="#icon-play" v-else></use>
    </svg>
</div>

上一首 & 下一首

<div class="player-controls__item" @click="prevTrack">
    <svg class="icon">
        <use xlink:href="#icon-prev"></use>
    </svg>
</div>

<div class="player-controls__item" @click="nextTrack">
    <svg class="icon">
        <use xlink:href="#icon-next"></use>
    </svg>
</div>

❤ 🔗

<div class="player-controls__item -favorite" 
     :class="{ active : currentTrack.favorited }" 
     @click="favorite">
    <svg class="icon">
        <use xlink:href="#icon-heart-o"></use>
    </svg>
</div>

<a :href="currentTrack.url" target="_blank" class="player-controls__item">
    <svg class="icon">
        <use xlink:href="#icon-link"></use>
    </svg>
</a>

歌曲信息 & 进度条

<div class="progress" ref="progress">
    
    <div class="progress__top">
        <div class="album-info" v-if="currentTrack">
            <div class="album-info__name">{{ currentTrack.artist }}</div>
            <div class="album-info__track">{{ currentTrack.name }}</div>
        </div>
        <div class="progress__duration">{{ duration }}</div>
    </div>
    
    <div class="progress__bar" @click="clickProgress">
        <div class="progress__current" :style="{ width : barWidth }"></div>
    </div>
    
    <div class="progress__time">{{ currentTime }}</div>
</div>

<div v-cloak></div>

CSS

重点就是 @media 的适配,没啥好说的,代码就放一点儿吧,想看的去Github吧,图中是另一种样式。

image.png

body {
  background: #dfe7ef;
  font-family: "Bitter", serif;
}

* {
  box-sizing: border-box;
}

.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  stroke-width: 0;
  stroke: currentColor;
  fill: currentColor;
}

.wrapper {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background-size: cover;
  @media screen and (max-width: 700px), (max-height: 500px) {
    flex-wrap: wrap;
    flex-direction: column;
  }
}

Vue

导入Github上找到的音源及Vue2.6:

<script src="https://cdn.jsdelivr.net/gh/nj-lizhi/song@master/audio/list.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>

整体框架

new Vue({
  el: "#app",
  data() {
    return {
      audio: null,
      circleLeft: null,
      barWidth: null,
      duration: null,
      currentTime: null,
      isTimerPlaying: false,
      tracks: list,    // 导入的音源
      currentTrack: null,
      currentTrackIndex: 0,
      transitionName: null
    };
  },
  methods: {
    play() {
      // 播放 & 暂停
    },
    generateTime() {
      // 时长
    },
    updateBar(x) {
      // 进度条
    },
    clickProgress(e) {
      // 点击进度条调节进度
    },
    prevTrack() {
      // 上一首
    },
    nextTrack() {
      // 下一首
    },
    resetPlayer() {
      // 重置
    },
    favorite() {
      // ❤
    }
  },
  created() {
    let vm = this;
    // 第一首歌固定是“忽然”
    this.currentTrack = this.tracks[3];
    this.audio = new Audio();
    this.audio.src = this.currentTrack.url;
    
    this.audio.ontimeupdate = function () {
        vm.generateTime();
    };
    this.audio.onloadedmetadata = function () {
        vm.generateTime();
    };
    
    // 放完自动切换下一首
    this.audio.onended = function () {
        vm.nextTrack();
        this.isTimerPlaying = true;
    };
  }
});

播放 & 暂停

play() {
    if (this.audio.paused) {
        this.audio.play();
        this.isTimerPlaying = true;
    } else {
        this.audio.pause();
        this.isTimerPlaying = false;
    }
},

时长

generateTime() {
    let width = (100 / this.audio.duration) * this.audio.currentTime;
    this.barWidth = width + "%";
    this.circleLeft = width + "%";
    let durmin = Math.floor(this.audio.duration / 60);
    let dursec = Math.floor(this.audio.duration - durmin * 60);
    let curmin = Math.floor(this.audio.currentTime / 60);
    let cursec = Math.floor(this.audio.currentTime - curmin * 60);
    if (durmin < 10) {
        durmin = "0" + durmin;
    }
    if (dursec < 10) {
        dursec = "0" + dursec;
    }
    if (curmin < 10) {
        curmin = "0" + curmin;
    }
    if (cursec < 10) {
        cursec = "0" + cursec;
    }
    this.duration = durmin + ":" + dursec;
    this.currentTime = curmin + ":" + cursec;
},

进度条

updateBar(x) {
    let progress = this.$refs.progress;
    let maxduration = this.audio.duration;
    let position = x - progress.offsetLeft;
    let percentage = (100 * position) / progress.offsetWidth;
    if (percentage > 100) {
        percentage = 100;
    }
    if (percentage < 0) {
        percentage = 0;
    }
    this.barWidth = percentage + "%";
    this.circleLeft = percentage + "%";
    this.audio.currentTime = (maxduration * percentage) / 100;
    this.audio.play();
},

点击进度条调节进度

clickProgress(e) {
    this.isTimerPlaying = true;
    this.audio.pause();
    this.updateBar(e.pageX);
},

上一首 & 下一首

我是这样设置的:下一首按随机播放来,上一首按顺序来 🏏

prevTrack() {
    this.transitionName = "scale-in";
    if (this.currentTrackIndex > 0) {
        this.currentTrackIndex--;
    } else {
        this.currentTrackIndex = this.tracks.length - 1;
    }
    this.currentTrack = this.tracks[this.currentTrackIndex];

    this.resetPlayer();
},

nextTrack() {
	this.transitionName = "scale-out";
    this.currentTrackIndex = parseInt(this.tracks.length * Math.random());
    this.currentTrack = this.tracks[this.currentTrackIndex];
    this.resetPlayer();
},

重置

每次切歌的时候重置:

resetPlayer() {
    this.barWidth = 0;
    this.circleLeft = 0;
    this.audio.currentTime = 0;
    this.audio.src = this.currentTrack.url;
    setTimeout(() => {
        if (this.isTimerPlaying) {
            this.audio.play();
        } else {
            this.audio.pause();
        }
    }, 300);
},

点红心的功能弃置了,因为是大佬整理的音源没有相关项,而且这个功能比较鸡肋,没有记忆,❎掉就没了。

favorite() {
    this.tracks[this.currentTrackIndex].favorited = !this.tracks[
        this.currentTrackIndex
    ].favorited;
}

Todo

还有一些有待完成的功能等我有时间再说吧😛:

  1. 暗黑模式
  2. 单曲循环,列表循环,列表顺序播放几种播放模式
  3. 列表显示全部歌曲
  4. ……

Summary

虽然我是个菜狗子,但还是被我整出来了。这就是站在前人的肩膀上吗,爱了爱了!

✔ 感谢以下大佬的开源项目:

如果对您有帮助,希望能给个👍评论收藏三连!

欢迎关注互相交流,有问题可以评论留言。

我是Mancuoj,更多有趣文章:Mancuoj 的个人主页