为博客添加 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>
大佬的说明写得很详细,很适合用来学习。