vue3.x 手摸手教学撸小型音乐播放器,开箱即用

925 阅读3分钟

前言

在业务开发的时候,以为网上有很多现成的播放器可以引入第三方的来使用,异想天开的觉得引入一下就可以改一改就可以实现了,没想到网上五花八门,根本使用不了美梦破碎,最后只能自己动手,手撸一个vue3.x ➕ ts版本的音乐播放器。 image.png

技术栈

vue3.x + ts 为了贴近前端的技术前沿,肯定要用最新的技术栈啦,使用vue3.x 的组合式api开发完成 代码聚合在一个useHooks中,别提有多爽了,清晰脱俗

传送门

github.com/vintonHuang… 目前没有办法做成npm包让大家使用,所以只能让大家如果有需要的话,可以直接在里面搬逻辑,修改音乐资源,修改样式。因为这种东西是在是太自定义了

话不多说,直接上代码,懂代码的自然懂,可以直接去下载仓库去看,会使用到一些工具类的函数。ts代码有注释,函数都有什么作用。相信你一看就能会,有什么建议可以提issues,一起讨论

vue文件模版代码

<!--
 * @Author: Vinton
 * @Date: 2022-04-29 14:35:24
 * @Description: 模版代码
-->
<template>
  <div class="audio-container">
    <div class="btn-change" @click="changeNextBatch"></div>
    <div class="music-image">
      <img
        class="animate-img"
        :src="importResource(currentMusicImageUrl)"
        alt=""
      />
    </div>
    <div class="process">
      <span class="process-startTime">{{ currentProcessTime }}</span>
      <span id="process" class="process-content" @click="changeProgress">
        <div
          class="process-content-line"
          :style="{ width: progressNum + '%' }"
        ></div>
        <div class="process-content-points"></div>
      </span>
      <span class="process-entTime">{{ currentTotalTime }}</span>
    </div>
    <div class="operation-btn">
      <div class="operation-btn-pre" @click="changePreMusic"></div>
      <div
        :class="[audioIsPlaying ? 'pause-btn' : 'play-btn']"
        @click="audioPlayOrPause"
      ></div>
      <div class="operation-btn-next" @click="changeNextMusic"></div>
    </div>
    <div class="music-content">
      <ul>
        <li
          v-for="(item, index) in currentAudioMenus"
          :key="index"
          :class="{ acitve: isActiveIndex === index }"
          @click="playMusicOnMusicList(index, item)"
        >
          <span v-if="isActiveIndex === index" class="music-wave"> </span>
          <span v-else>{{ index + 1 }}</span>
          <span>{{ item.name }}</span>
          <span>{{ item.author }}</span>
          <span>{{ item.duration }}</span>
        </li>
      </ul>
    </div>
  </div>
  <audio
    class="music-feast"
    :src="importResource(currentMusicSrc)"
    preload="auto"
    :autoPlay="false"
    style="display: none"
    @timeupdate="timeupdate"
    @ended="playEnded"
    @loadedmetadata="loadedMetaData"
  ></audio>
</template>
<script lang="ts">
export default {
  name: "AudioModel",
};
</script>
<script setup lang="ts">
import { useAudioAction } from "./composables/useAudioAction";
import { useImportResForOss } from "@/hooks/useImportResForOss";
const { importResource } = useImportResForOss();
const {
  currentAudioMenus,
  isActiveIndex,
  audioIsPlaying,
  audioPlayOrPause,
  playMusicOnMusicList,
  currentMusicImageUrl,
  currentMusicSrc,
  changePreMusic,
  changeNextMusic,
  currentProcessTime,
  timeupdate,
  playEnded,
  loadedMetaData,
  currentTotalTime,
  progressNum,
  seeked,
  changeProgress,
  changeNextBatch,
} = useAudioAction();
</script>
<style scoped lang="less">
@import url("../../styles/mixin.less");
.audio-container {
  .main-bg(750px,1086px,"@/assets/bg_03.jpg");
  .btn-change {
    position: relative;
    left: 600px;
    top: 10px;
    .main-bg(142px, 49px, "@/assets/btn-change.png");
  }
  .music-image {
    margin: 0 auto;
    .main-bg(367px, 367px, "@/assets/cover_bg.png");
    position: relative;
    img {
      position: absolute;
      left: 42px;
      top: 40px;
      width: 280px;
      height: 280px;
    }
  }
  .process {
    font-size: 22px;
    color: #a64c3c;
    line-height: 56px;
    cursor: pointer;
    display: flex;
    align-items: center;
    position: relative;
    left: 42px;
    &-content {
      width: 520px;
      height: 2px;
      border: 2px solid #dbb0a6;
      margin: 0 18px;
      display: flex;
      align-items: center;
      &-line {
        height: 2px;
        background: #b7594c;
      }
      &-points {
        width: 15px;
        height: 15px;
        background: #b7594c;
        border-radius: 50%;
      }
    }
  }
  .operation-btn {
    display: flex;
    align-items: center;
    position: relative;
    left: 245px;
    &-pre {
      .main-bg(37px, 33px, "@/assets/icon_pre.png");
    }
    .play-btn {
      margin: 0 51px;
      .main-bg(85px, 85px, "@/assets/icon_start.png");
    }
    .pause-btn {
      margin: 0 51px;
      .main-bg(85px, 85px, "@/assets/icon_pause.png");
    }
    &-next {
      .main-bg(36px, 33px, "@/assets/icon_next.png");
    }
  }
  .music-content {
    width: 735px;
    height: 235px;
    opacity: 0.72;
    background: #fffdf1;
    border-radius: 3px;
    margin: 22px auto;
    padding-top: 15px;
    ul {
      display: flex;
      flex-direction: column;
      justify-content: space-around;
      font-size: 22px;
      text-align: left;
      line-height: 52px;
      color: #b6594b;
      li {
        cursor: pointer;
        .music-wave {
          .main-bg(25px, 25px, "@/assets/voice.png");
        }
        display: flex;
        justify-content: space-around;
        text-align: left;
        align-items: center;
        span:nth-child(2) {
          width: 200px;
        }
        span:nth-child(3) {
          width: 200px;
        }
        span:nth-child(4) {
          width: 50px;
        }
        &.acitve {
          color: #dfb155;
        }
      }
    }
  }
}
</style>

组合式音乐播放器代码

/*
 * @Author: Vinton
 * @Date: 2022-03-31 22:02:35
 * @Description: 音乐播放器逻辑代码
 */
import FeastService from "@/service/audio";
import { audioInfo } from "@/interfaces/audio";
import { onMounted, ref, watch, computed, reactive, nextTick } from "vue";
import { formatSeconds } from "@/utils/index";
export const useAudioAction = () => {
  const audioMenus = ref<Array<audioInfo>[]>([]);
  const audioIndex = ref<number>(0); // 批次
  let audio: HTMLAudioElement | undefined;
  let image: HTMLImageElement | undefined;
  onMounted(async () => {
    const { state, data } = await FeastService.getAudioInfo();
    if (state === 200) {
      audioMenus.value = data.data;
    }
    audio = Array.from(document.getElementsByTagName("audio"))?.find(
      (item) => item.className === "music-feast"
    );
    image = Array.from(document.getElementsByTagName("img"))?.find(
      (item) => item.className === "animate-img"
    );
    nextTick(() => {
      totalProcess = document.getElementById("process");
    });
  });
  const currentAudioMenus = computed(() => {
    return audioMenus.value[audioIndex.value];
  });
  // 切换下一首歌曲
  const changeNextBatch = () => {
    handlePlayEnd();
    audioIndex.value += 1;
  };
  // 控制下一批次可以循环到第一批次
  watch(
    () => audioIndex.value,
    (newVal) => {
      if (newVal > audioMenus.value.length - 1) {
        audioIndex.value = 0;
      }
    }
  );

  const isActiveIndex = ref<number>(-1);
  const audioIsPlaying = ref<boolean>(false);
  const currentMusicItem = reactive<audioInfo>({} as audioInfo);
  let flag: number;
  let timer: NodeJS.Timeout;
  // 旋转图片
  const rotate = () => {
    let deg = 0;
    flag = 1;
    timer = setInterval(function () {
      if (image != undefined) {
        image.style.transform = "rotate(" + deg + "deg)";
      }
      deg += 1;
      if (deg > 360) {
        deg = 0;
      }
    }, 30);
  };
  const imagePause = () => {
    clearInterval(timer);
    flag = 0;
  };
  // 点击底下的按钮播放音乐和暂停音乐
  const audioPlayOrPause = () => {
    audioIsPlaying.value = !audioIsPlaying.value;
    if (!currentMusicItem.audioSrc) {
      // 一进入页面,进行点击了暂停播放按钮,默认播放第一首歌
      setCurrentAudioRes(currentAudioMenus.value[0]);
      isActiveIndex.value = 0;
      rotate();
      return;
    }
    if (flag) {
      imagePause();
      audio?.pause();
    } else {
      rotate();
      audio?.play();
    }
  };
  // 点击列表播放音乐
  const playMusicOnMusicList = (index: number, item: audioInfo) => {
    if (isActiveIndex.value === index) {
      return;
    }
    imagePause();
    setCurrentAudioRes(item);
    audioIsPlaying.value = true;
    isActiveIndex.value = index;
    rotate();
  };
  // 点击上一首音乐
  const changePreMusic = (): void => {
    if (!currentMusicItem.audioSrc) {
      // 如果当前没有音乐选择播放就直接操作无效
      return;
    }
    if (isActiveIndex.value == 0) {
      playMusicOnMusicList(3, currentAudioMenus.value[3]);
    } else {
      playMusicOnMusicList(
        isActiveIndex.value - 1,
        currentAudioMenus.value[isActiveIndex.value - 1]
      );
    }
  };
  // 点击下一首音乐
  const changeNextMusic = (): void => {
    if (!currentMusicItem.audioSrc) {
      return;
    }
    if (isActiveIndex.value == 3) {
      playMusicOnMusicList(0, currentAudioMenus.value[0]);
    } else {
      playMusicOnMusicList(
        isActiveIndex.value + 1,
        currentAudioMenus.value[isActiveIndex.value + 1]
      );
    }
  };
  // 设置当前播放音乐的资源
  const setCurrentAudioRes = (item: audioInfo): void => {
    currentMusicItem.url = item.url;
    currentMusicItem.name = item.name;
    currentMusicItem.duration = item.duration;
    currentMusicItem.author = item.author;
    currentMusicItem.audioSrc = item.audioSrc;
  };
  const currentMusicImageUrl = computed(() => {
    return currentMusicItem.url
      ? currentMusicItem.url
      : "/resource/assets/audio/01/3/Juvenile.png";
  });
  const currentMusicSrc = computed(() => {
    return currentMusicItem.audioSrc ? currentMusicItem.audioSrc : "";
  });

  const handleTimeFormat = (value: number) => {
    if (!value) {
      return "00:00";
    }
    let timeInfo = formatSeconds(value);
    if (timeInfo.substring(0, 2) === "00") {
      timeInfo = timeInfo.substring(3);
    }
    return timeInfo;
  };

  const currTime = ref<number>(0);
  const currentProcessTime = computed(() => {
    return handleTimeFormat(currTime.value);
  });

  const totalTime = ref<number>(0);
  const currentTotalTime = computed(() => {
    return handleTimeFormat(totalTime.value);
  });
  // 当前歌曲资源的总时长
  const loadedMetaData = (e: any) => {
    totalTime.value = parseInt(e.currentTarget.duration);
  };
  // 目前歌曲播放到哪里的时间
  const timeupdate = (e: any) => {
    currTime.value = parseInt(e?.currentTarget?.currentTime);
  };

  // 音乐播放完毕需要清空播放资源数据
  const playEnded = () => {
    handlePlayEnd();
  };
  const handlePlayEnd = () => {
    audioIsPlaying.value = false;
    isActiveIndex.value = -1;
    imagePause();
    // 清空当前数据
    setCurrentAudioRes({
      url: "",
      audioSrc: "",
      duration: "",
      name: "",
      author: "",
    });
  };
  // 播放进度
  const progressNum = computed(() => {
    if (!currTime.value || !totalTime.value) {
      return 0;
    }
    return (currTime.value / totalTime.value) * 100;
  });
  // 跳转歌曲进度
  const seeked = (progressNum: number) => {
    const seekTime = totalTime.value * (progressNum / 100);
    if (audio !== undefined) {
      audio.currentTime = seekTime;
    }
  };
  let totalProcess: HTMLElement | null;
  const changeProgress = (e: any) => {
    if (!audioIsPlaying.value) {
      return;
    }
    let progressPercent: number;
    if (totalProcess !== null) {
      progressPercent =
        ((e.pageX - totalProcess?.getBoundingClientRect().left) /
          totalProcess?.getBoundingClientRect().width) *
        100;
      seeked(progressPercent > 0 ? progressPercent : 0);
    }
  };
  return {
    currentAudioMenus,
    isActiveIndex,
    audioIsPlaying,
    audioPlayOrPause,
    playMusicOnMusicList,
    currentMusicImageUrl,
    currentMusicSrc,
    changePreMusic,
    changeNextMusic,
    currentProcessTime,
    timeupdate,
    playEnded,
    loadedMetaData,
    currentTotalTime,
    progressNum,
    seeked,
    changeProgress,
    changeNextBatch,
  };
};

结束语

我是黄老师要变胖,一位卑微前端打工仔,分享一个在开发中,手撸的音乐小组件,希望可以帮到你,最好是只要cv就可以完事,早早下班,早早摸鱼🐟