Vue3.0学习完成一个音乐播放器(持续更)

1,418 阅读3分钟

前言:

Vue3.0发布一年多了,然而我还没用过,想学,但是很懒,不知道从何开始,不能这么堕落下去了,突然想到以前用vue2写过一个音乐播放器,这次就以音乐播放器为起点,一边学习一边用3.0写一个音乐播放器。 基于vue2.x构建的音乐播放器样式以及歌词滚动

参考了大佬的github.com/maomao1996/…
本文项目地址(未全部完成):gitee.com/myDearDer/b…

写的不好的地方希望大佬指正

2022/1/12

vite创建项目

npm init @vitejs/app music-player

搭配路由,less,axios,element-plus。

image.png

先从简单的开始吧,图形界面。前期做的简单一些,实现播放,搜索。

2022/1/17

完成界面,支持点击播放音乐,歌词滚动,进度条。效果有点简陋,如下,为了方便看得清,调了一下背景色。

QQ图片20220118104029.png

简单说说思路,首先布局分顶部,中间的内容,和底部的进度条三大部分。 中间四个切换我准备写成组件,说一下播放的实现。

先初始化第一条要播放的歌曲信息

先定义一些信息,暂时先用假数据代替。 代码都比较简单,基础,我都有写注释,就不细说了。

  setup() {
    const state = reactive({
      backgroundUrl: "", // 模糊背景图
      menuList: ["正在播放", "推荐", "搜索", "播放历史"],
      activeIndex: 0, // 选中
      playing: false, // 是否正在播放
      songList: [
        {
          albumId: 122397809,
          albumTitle: "一些古风歌【2021】",
          artistsName: "平生不晚",
          cover:
            "http://localhost:8080/pOR45DW9BfLSQ2JDJJeUgQ==/109951165714496390.jpg",
          id: 1820643403,
          name: "伯虎说(纯戏腔段)",
          url: "http://localhost:8080/song/media/outer/url?id=1820643403.mp3",
        },
      ],
      audioProgress: 0, // 进度
      playType: 1, // 播放类型,单曲循环还是顺序播放
      playIndex: 0, // 当前播放哪一首
      thumbTranslateX: 0,
      lyricIndex: 0, // 歌词到哪一行了
      currentTime: 0, // 当前播放进度
      progressL: 0, // 进度条总长度
      songInfo: {}, // 歌曲详情
      lyricInfo: [], // 歌词信息
      audioTime: "00:00", // 歌曲时长
      volume: 80, // 音量
    });
    let track = ref(null);
    onMounted(() => {
      const audio = document.getElementById("audio");
      console.log(track.value); //取元素宽高等属性操作
      state.progressL = track.value.offsetHeight;
      Init(); // 初始化
    });
    const Init = () => {
      GetSongInfo();
    };
    const GetSongInfo = () => {
      let myList = state.songList;
      state.songInfo = myList[0];
      state.backgroundUrl = state.songInfo.cover
      audioInit(); // 初始化audio
      GetLyric(state.songInfo.id); // 获取歌词信息
    };
    const audioInit = () => {
      let progressL = track.value.offsetWidth; // 进度条总长
      audio.addEventListener("canplay", () => {
        state.audioTime = TimeToString(audio.duration);
      });
      audio.addEventListener("timeupdate", () => {
        // 当前播放时间
        let compareTime = audio.currentTime;
        for (let i = 0; i < state.lyricInfo.length; i++) {
          if (compareTime > parseInt(state.lyricInfo[i].time)) {
            const index = state.lyricInfo[i].index;
            if (i === parseInt(index)) {
              state.lyricIndex = i; // 获取当前播放歌词是哪一条
            }
          }
        }
        state.currentTime = TimeToString(audio.currentTime);
        state.audioProgress = audio.currentTime / audio.duration;
        state.thumbTranslateX = (state.audioProgress * progressL).toFixed(3);
      });
      audio.addEventListener("ended", () => {
        switch (parseInt(state.playType)) {
          case 1: // 列表循环
            state.playIndex =
              state.playIndex + 1 >= state.songList.length
                ? 0
                : state.playIndex + 1;
            break;
          case 2: // 随机播放
            state.playIndex = Math.floor(Math.random() * state.songList.length);
            break;
          case 3: // 单曲循环
            break;
        }
        state.songInfo = state.songList[state.playIndex];
        GetLyric(state.songInfo.id);
        setTimeout(() => {
          // rotate.style.animationPlayState = "running";
          audio.play();
        }, 100);
      });
    };
    // 获取歌词信息生成歌词list
    const GetLyric = (id) => {
      liricApi
        .GetLiricInfo({
          id,
        })
        .then((res) => {
          console.log("res", res);
          let lrc = res.lrc.lyric;
          GetLyricList(lrc);
        });
    };
    const GetLyricList = (lrc) => {
      let lyricsObjArr = [];
      const regNewLine = /\n/;
      const lineArr = lrc.split(regNewLine); // 每行歌词的数组
      const regTime = /\[\d{2}:\d{2}.\d{2,3}\]/;
      lineArr.forEach((item) => {
        if (item === "") return;
        const obj = {};
        const time = item.match(regTime);

        obj.lyric =
          item.split("]")[1].trim() === "" ? "" : item.split("]")[1].trim();
        obj.time = time
          ? TimeToSeconds(time[0].slice(1, time[0].length - 1))
          : 0;
        obj.uid = Math.random().toString().slice(-6);
        if (obj.lyric === "") {
          console.log("这一行没有歌词");
        } else {
          lyricsObjArr.push(obj);
        }
      });
      state.lyricInfo = lyricsObjArr.map((item, index) => {
        item.index = index;
        return {
          ...item,
        };
      });
      console.log("state.lyricInfo", state.lyricInfo);
    };
    const TimeToSeconds = (time) => {
      // 格式化歌词的时间 转换成 sss:ms
      const regMin = /.*:/;
      const regSec = /:.*\./;
      const regMs = /\./;

      const min = parseInt(time.match(regMin)[0].slice(0, 2));
      let sec = parseInt(time.match(regSec)[0].slice(1, 3));
      const ms = time.slice(
        time.match(regMs).index + 1,
        time.match(regMs).index + 3
      );
      if (min !== 0) {
        sec += min * 60;
      }
      return Number(sec + "." + ms);
    };
    const ChangeActive = (index) => {
      state.activeIndex = index;
    };
    const TimeToString = (seconds) => {
      let param = parseInt(seconds);
      let hh = "",
        mm = "",
        ss = "";
      if (param >= 0 && param < 60) {
        param < 10 ? (ss = "0" + param) : (ss = param);
        return "00:" + ss;
      } else if (param >= 60 && param < 3600) {
        mm = parseInt(param / 60);
        mm < 10 ? (mm = "0" + mm) : mm;
        param - parseInt(mm * 60) < 10
          ? (ss = "0" + String(param - parseInt(mm * 60)))
          : (ss = param - parseInt(mm * 60));
        return mm + ":" + ss;
      }
    };
    const playMusic = () => {
      if (state.playing) {
        // 播放中,点击则为暂停
        state.playing = false;
        // rotate.style.animationPlayState = "paused";
        audio.pause();
      } else {
        // 暂停中,点击则为播放
        state.playing = true;
        // rotate.style.animationPlayState = "running";
        audio.play();
      }
    };
    const audioProgressPercent = computed(() => {
      return `${state.audioProgress * 100}%`;
    });
    
    return {
      ...toRefs(state),
      ChangeActive,
      playMusic,
      track,
      audioProgressPercent,
    };
}

主要的HTML

      <div class="play-icon-container">
        <img class="play-icon" src="../assets/arrow_01.png" alt="上一曲" />
        <img
          v-show="!playing"
          @click="playMusic"
          class="play-icon"
          src="../assets/play_01.png"
          alt="播放"
        />
        <img
          v-show="playing"
          @click="playMusic"
          class="play-icon"
          src="../assets/play_02.png"
          alt="暂停"
        />
        <img class="play-icon" src="../assets/arrow_02.png" alt="下一曲" />
      </div>

组件

        <div class="main-container">
          <Playing :songInfo="songInfo" :lyricIndex="lyricIndex" :lyricInfo="lyricInfo"></Playing>
        </div>

组件具体实现

<template>
  <div class="song-cover-lyric">
    <div class="disc-continer">
      <div class="poster" ref="rotate">
        <img :src="songInfo.cover" alt="" />
      </div>
      <div class="song-name">{{ songInfo.name }}</div>
      <div class="song-artistsName">{{ songInfo.artistsName }}</div>
    </div>
    <div class="lyric">
      <div
        ref="musicLyric"
        class="music-lyric"
        :style="{ 'padding-top': paddingTop }"
      >
        <div class="music-lyric-items" :style="lyricTop">
          <template v-if="lyricInfo.length > 0">
            <p
              v-for="(item, index) in lyricInfo"
              :key="index"
              :data-index="index"
              ref="lyric"
              :style="{
                color: lyricIndex === index ? colorLight : color,
                'font-size': fontSize,
              }"
            >
              {{ item.lyric }}
            </p>
          </template>
          <p style="color: #fff" v-else>文案加载失败!</p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { reactive, ref, computed, onMounted, toRefs } from "vue";
// import axios from "../api/request"
export default {
  props: {
    songInfo: {
      type: Object,
      default: () => [],
    },
    lyricInfo: {
      type: Object,
      default: () => [],
    },
    lyricIndex: {
      type: Number,
      default: 0,
    },
  },
  setup(props, { emit }) {
    console.log(props.lyricInfo);
    const state = reactive({
      lyricInfo: [],
      audioProgress: 0,
      thumbTranslateX: 0,
      top: 0,
      color: "#fff", //文案默认颜色
      colorLight: "#40ce8f", //文案高亮色
      fontSize: "16px", //文案字体大小
      lineHeight: "42", //每段行高
      paddingTop: "200px", //高亮文案部分居中
    });
    // let rotate = ref(null);
    onMounted(() => {
      // GetLyric(props.songInfo.id);
    });
    // 计算歌词滚动
    const lyricTop = computed (()=> {
      return `transform :translate3d(0, ${(0 - state.lineHeight) *
        (props.lyricIndex - state.top)}px, 0);color: ${state.color};line-height: ${
        state.lineHeight
      }px`;
    })
    return {
      ...toRefs(state),
      lyricTop
    };
  },
};
</script>
<style lang="less" scoped>
.song-cover-lyric {
  position: relative;
  width: 100%;
  height: 100%;
  padding-bottom: 72px;
  box-sizing: border-box;
  display: flex;
  overflow: hidden;

  .disc-continer {
    width: 50%;
    height: 100%;
    position: relative;

    .poster {
      position: relative;
      width: 280px;
      height: 280px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.3);
      left: 50%;
      top: 80px;
      margin-left: -140px;
      box-shadow: 0 0 0 12px rgba(255, 255, 255, 0.4);
      animation: animations1 12s linear infinite forwards;
      animation-play-state: paused;
      overflow: hidden;
      margin-bottom: 120px;

      img {
        width: 100%;
        height: 100%;
      }
    }

    .song-name {
      width: 100%;
      height: 40px;
      text-align: center;
      font-size: 32px;
      font-weight: 600;
      color: #fff;
      line-height: 40px;
    }

    .song-artistsName {
      width: 100%;
      height: 40px;
      text-align: center;
      font-size: 28px;
      font-weight: 600;
      color: #fff;
      line-height: 40px;
      margin-top: 24px;
    }

    @keyframes animations1 {
      from {
        transform: rotate(0deg);
      }

      to {
        transform: rotate(360deg);
      }
    }
  }

  .lyric {
    width: 50%;
    height: 590px;
    position: relative;
    overflow: hidden;
    padding-top: 84px;
    box-sizing: border-box;

    .music-lyric {
      // position: absolute;
      // top: 1.9rem;
      // right: 0;
      // bottom: 0;
      // left: 0;
      // padding-top: 300px;
      box-sizing: border-box;
      overflow: hidden;
      text-align: center;
      mask-image: linear-gradient(
        to bottom,
        rgba(255, 255, 255, 0) 0,
        rgba(255, 255, 255, 0.6) 15%,
        rgba(255, 255, 255, 1) 25%,
        rgba(255, 255, 255, 1) 75%,
        rgba(255, 255, 255, 0.6) 85%,
        rgba(255, 255, 255, 0) 100%
      );

      .music-lyric-items {
        text-align: center;
        font-size: 16px;
        color: #fff;
        transform: translate3d(0, 0, 0);
        transition: transform 0.6s ease-out;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;

        .on {
          color: #40ce8f;
        }

        P {
          margin: 0;
          padding: 0;
        }
      }
    }
  }
}
</style>