Skip to content

为博客添加 MetingJS 音乐

1. 起因

因为博客的阅读页更偏向于文档风格,所以就一直在思考该怎么增加阅读体验,加一个音乐播放器是一个不错的选择,刚好发现 curve 主题发布,不管是流畅度还是设计感都非常棒。但是在上手体验一番之后发现可能还是因为刚发布不完善的原因,有长文章加载慢和不支持 code-group 的问题,配置与修改的地方也比当前的多得多,更不用说大改主页的样式 (目前的主页还是挺喜欢的),有些太耗时了,所以暂时还是不碰了,看日后的更新情况。于是就有了从 curve 缝一个 Player 组件过来的想法。

2. 代码

Player 组件的实质是对 Aplayer 的美化与加工

文件:theme/index.ts

ts
import BlogTheme from '@sugarat/theme'
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import ImgBlur from './components/ImgBlur.vue'
import Player from './components/Player.vue'
import { h, render } from 'vue'

// pinia
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

export default {
    ...BlogTheme,
    enhanceApp({ app }) {
        app.use(pinia);
        app.component('Player', Player);
        const playerVNode = h(Player);
        try { // 避免服务端环境找不到BOM引发报错
            // 将 VNode 渲染到指定的 DOM 容器中
            render(playerVNode, document.body);
        } catch (error) {
            console.log(error);
        }
    }
}

在一通操作后就能正常显示 Player.vue 组件了,剩下的就是对组件的修改和适配(store, style, server...),再把调用 api 的函数给揉进组件,就完成了组件的拆分和引入了。

以下是 Player.vue 的原代码:https://github.com/imsyy/vitepress-theme-curve/blob/master/.vitepress/theme/components/Player.vue

Player.vue 完整代码
vue
<!-- 全局播放器 -->
<template>
  <div
    v-if="playerShow"
    :class="['player', { playing: playState }]"
    @click="player?.toggle()"
  >
    <div ref="playerDom" class="player-content" />
  </div>
</template>

<script setup>
  import { storeToRefs } from "pinia";
  import { mainStore } from "../store/index";
  import { ref, watch, onMounted, onBeforeUnmount } from "vue";
  import "aplayer/dist/APlayer.min.css";

  /**
   * Meting
   * @param {id} string - 歌曲ID
   * @param {server} string - 服务器
   * @param {type} string - 类型
   * @returns {Promise<Object>} - 音乐详情
   */
  const getMusicList = async (id, server = "netease", type = "playlist") => {
    const result = await fetch(
      //我的(能有免费的netease)
      // `https://metingjsapi.vercel.app/api?server=${server}&type=${type}&id=${id}`,
      // 官方(仅netease)
      // `http://localhost:3000/api?server=${server}&type=${type}&id=${id}`,
      // anzhiyu接口(能用qq)
      // `https://meting.qjqq.cn/?server=${server}&type=${type}&id=${id}`,
      // 圆弧派接口(netease 有VIP)
      `https://v.iarc.top/?server=${server}&type=${type}&id=${id}`
    );
    const list = await result.json();
    return list.map((song) => {
      const { pic, ...data } = song;
      return {
        ...data,
        cover: pic,
      };
    });
  };

  const store = mainStore();
  const enable = true;
  const id = 7387315194;
  const server = "netease";
  const type = "playlist";
  const { playerShow, playerVolume, playState, playerData } =
    storeToRefs(store);

  // APlayer
  const player = ref(null);
  const playerDom = ref(null);

  // 获取播放列表
  const getMusicListData = async () => {
    try {
      const musicList = await getMusicList(id, server, type);
      console.log(musicList);
      if (musicList[1].url == "") {
        let midArr = JSON.parse(
          decodeURIComponent(musicList[0].url.split("data=")[1])
        ).req_0.param.songmid;
        musicList.map((song, index) => {
          song.url = `https://meting.qjqq.cn/?server=tencent&type=url&id=${midArr[index]}`;
          return song;
        });
      }
      initAPlayer(musicList?.length ? musicList : []);
    } catch (error) {
      console.log("获取播放列表失败,请重试");
      initAPlayer([]);
    }
  };

  // 初始化播放器
  const initAPlayer = async (list) => {
    try {
      const playlist = [...list];
      if (!playlist?.length) return false;
      const module = await import("aplayer");
      const APlayer = module.default;
      player.value = new APlayer({
        container: playerDom.value,
        volume: playerVolume.value,
        lrcType: 3,
        listFolded: true,
        order: "random",
        audio: playlist,
      });
      console.info("🎵 播放器挂载完成", player.value);
      // 播放器事件
      player.value?.on("canplay", () => {
        // 更新信息
        getMusicData();
      });
      player.value?.on("play", () => {
        console.log("开始播放");
        playState.value = true;
      });
      player.value?.on("pause", () => {
        console.log("暂停播放");
        playState.value = false;
      });
      getMusicData();
      // 挂载播放器
      window.$player = player.value;
    } catch (error) {
      console.error("初始化播放器出错:", error);
    }
  };

  // 获取当前播放歌曲信息
  const getMusicData = () => {
    try {
      if (!playerDom.value) return false;
      const songInfo = playerDom.value.querySelector(".aplayer-info");
      // 歌曲信息
      const songName = songInfo.querySelector(".aplayer-title").innerText;
      const songArtist = songInfo
        .querySelector(".aplayer-author")
        .innerText.replace(" - ", "");
      console.log(songName, songArtist);
      // 更新信息
      playerData.value = {
        name: songName || "未知曲目",
        artist: songArtist || "未知艺术家",
      };
      // 更新媒体信息
      initMediaSession(playerData.value?.name, playerData.value?.artist);
    } catch (error) {
      console.error("获取播放信息出错:", error);
    }
  };

  // 初始化媒体会话控制
  const initMediaSession = (title, artist) => {
    if ("mediaSession" in navigator) {
      // 歌曲信息
      navigator.mediaSession.metadata = new MediaMetadata({ title, artist });
      // 按键关联
      navigator.mediaSession.setActionHandler("play", () => {
        player.value?.play();
      });
      navigator.mediaSession.setActionHandler("pause", () => {
        player.value?.pause();
      });
      navigator.mediaSession.setActionHandler("previoustrack", () => {
        player.value?.skipBack();
      });
      navigator.mediaSession.setActionHandler("nexttrack", () => {
        player.value?.skipForward();
      });
    }
  };

  // 监听播放器开启状态
  watch(
    () => playerShow.value,
    (val) => {
      if (!val) return false;
      player.value?.destroy();
      getMusicListData();
    }
  );

  // 监听播放器音量变化
  watch(
    () => playerVolume.value,
    (val) => {
      player.value?.volume(val, true);
    }
  );

  onMounted(() => {
    if (window.innerWidth >= 768 && playerShow.value && enable)
      getMusicListData();
  });

  onBeforeUnmount(() => {
    player.value?.destroy();
  });
</script>

<style lang="scss" scoped>
  .player {
    height: 42px;
    margin-top: 12px;
    transition: transform 0.3s;
    cursor: pointer;
    position: fixed;
    left: 10px;
    bottom: 10px;
    z-index: 9999;
    .player-content {
      margin: 0;
      width: fit-content;
      border-radius: 50px;
      overflow: hidden;
      color: var(--vp-c-text-1);
      font-family: HarmonyOS;
      background-color: rgba(var(--bg-gradient));
      border: 1px solid #3d3d3f63;
      box-shadow: var(--box-shadow);
      transition: all 0.3s;
      :deep(.aplayer-body) {
        display: flex;
        flex-direction: row;
        align-items: center;
        padding: 6px;
        padding-right: 12px;
        pointer-events: none;
        .aplayer-pic {
          width: 30px;
          height: 30px;
          min-width: 30px;
          border-radius: 50%;
          margin-right: 8px;
          outline: 1px solid #3d3d3f63;
          animation: rotate 20s linear infinite;
          animation-play-state: paused;
          z-index: 2;
          .aplayer-button {
            display: none;
          }
        }
        .aplayer-info {
          display: flex;
          flex-direction: row;
          align-items: center;
          height: auto;
          margin: 0;
          padding: 0;
          border: none;
          .aplayer-music {
            margin: 0;
            padding: 0;
            height: auto;
            display: flex;
            line-height: normal;
            z-index: 2;
            .aplayer-title {
              line-height: normal;
              display: inline-block;
              white-space: nowrap;
              max-width: 120px;
              overflow: hidden;
              text-overflow: ellipsis;
            }
            .aplayer-author {
              display: none;
            }
          }
          .aplayer-lrc {
            margin: 0;
            opacity: 0;
            margin-left: 12px;
            width: 0;
            z-index: 2;
            transition: width 0.3s, opacity 0.3s;
            &::before,
            &::after {
              display: none;
            }
            .aplayer-lrc-contents {
              p {
                text-align: center;
                color: #1b1c20de;
                filter: blur(0.8px);
                transition: filter 0.3s, opacity 0.3s;
                &.aplayer-lrc-current {
                  filter: blur(0);
                }
              }
            }
          }
          .aplayer-controller {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 0;
            .aplayer-time {
              display: none;
            }
            .aplayer-bar-wrap {
              margin: 0;
              padding: 0;
              opacity: 0;
              transition: opacity 0.3s;
              .aplayer-bar {
                height: 100%;
                background: transparent;
                .aplayer-loaded {
                  display: none;
                }
                .aplayer-played {
                  height: 100%;
                  background: #ffffff40 !important;
                  transition: width 0.3s;
                }
              }
            }
          }
        }
        .aplayer-notice,
        .aplayer-miniswitcher {
          display: none;
        }
      }
      :deep(.aplayer-list) {
        display: none;
      }
      &::after {
        content: "播放音乐";
        position: absolute;
        top: 0;
        left: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 100%;
        font-size: 14px;
        opacity: 0;
        color: #1b1c20de;
        background-color: var(--el-color-primary);
        pointer-events: none;
        transition: opacity 0.3s;
        z-index: 3;
      }
      &:hover {
        border-color: var(--el-color-primary);
        box-shadow: 0 8px 16px -4px #f2b94b23;
        &::after {
          opacity: 1;
        }
      }
    }
    &.playing {
      .player-content {
        color: #1b1c20de;
        background-color: var(--el-color-primary);
        border: 1px solid var(--el-color-primary);
        :deep(.aplayer-body) {
          .aplayer-pic {
            animation-play-state: running;
          }
          .aplayer-info {
            .aplayer-lrc {
              opacity: 1;
              width: 200px;
            }
            .aplayer-controller {
              .aplayer-bar-wrap {
                opacity: 1;
              }
            }
          }
        }
        &::after {
          opacity: 0;
        }
      }
    }
    &:active {
      transform: scale(0.98);
    }
    @media (max-width: 768px) {
      display: none;
    }
  }
</style>

大佬的说明写得很详细,很适合用来学习。

上次更新于: