VuePress 集成网易云音乐播放器完整实现
大约 12 分钟
VuePress 集成网易云音乐播放器完整实现
本文档详细介绍如何在 VuePress 2 + vuepress-theme-hope 主题中集成一个支持拖拽的网易云音乐播放器
📋 目录
✨ 功能特性
已实现功能
- ✅ 网易云音乐集成 - 支持播放网易云、QQ音乐、酷狗音乐等平台
- ✅ 拖拽移动 - 可自由拖拽到屏幕任意位置
- ✅ 展开/收起 - 支持最小化为悬浮按钮
- ✅ 位置记忆 - 刷新页面后保持上次的位置和状态
- ✅ 歌词显示 - 实时同步显示歌词
- ✅ 播放列表 - 显示完整歌单
- ✅ 响应式设计 - 适配PC端和移动端
- ✅ 暗黑模式 - 自动适配暗黑主题
- ✅ 自定义主题色 - 可自定义播放器颜色
效果预览
展开状态:
┌─────────────────────────────────┐
│ 🎵 拖动 [收起] │ ← 可拖拽的头部
├─────────────────────────────────┤
│ ▶ 歌曲名 - 歌手 │
│ ━━━━●━━━━━━━━━━ 2:30 / 4:20 │
│ 🔊 ━━●━━━━ │
│ │
│ 播放列表: │
│ 1. 歌曲1 │
│ 2. 歌曲2 │
│ 3. 歌曲3 │
└─────────────────────────────────┘
收起状态:
┌──────┐
│ 🎵 │ ← 可拖拽
└──────┘
🔧 技术选型
核心依赖
| 依赖包 | 版本 | 作用 |
|---|---|---|
aplayer | ^1.10.1 | HTML5 音乐播放器 |
meting | ^2.0.2 | 音乐平台 API 封装 |
技术栈
- VuePress: 2.0.0-beta.61
- Vue 3: 3.2.47
- TypeScript: 支持
- SCSS: 样式预处理器
📦 安装依赖
1. 安装音乐播放器相关包
npm install aplayer meting --save
2. 验证安装
安装完成后,package.json 应该包含:
{
"dependencies": {
"aplayer": "^1.10.1",
"meting": "^2.0.2"
}
}
💻 核心实现
文件结构
src/.vuepress/
├── components/
│ ├── MusicPlayer.vue # 音乐播放器核心组件(支持拖拽)
│ └── GlobalMusicPlayer.vue # 全局包装组件
├── config/
│ └── musicConfig.ts # 配置文件(可选)
└── client.ts # 客户端配置(注册全局组件)
1. 创建核心播放器组件
文件路径: src/.vuepress/components/MusicPlayer.vue
点击展开完整代码(约 445 行)
<template>
<div
class="music-player-wrapper"
v-if="isShow"
:style="playerPosition"
@mousedown="startDrag"
@touchstart="startDrag"
>
<!-- 音乐播放器容器 -->
<div class="music-player-container" :class="{ 'minimized': isMinimized, 'dragging': isDragging }">
<!-- 拖拽手柄 + 折叠/展开按钮 -->
<div class="player-header" :class="{ 'minimized-header': isMinimized }">
<div class="drag-handle" :title="'拖拽移动播放器'">
<i class="iconfont icon-drag"></i>
<span v-if="!isMinimized" class="drag-text">拖动</span>
</div>
<div class="player-toggle" @click.stop="togglePlayer" :title="isMinimized ? '展开播放器' : '收起播放器'">
<i class="iconfont" :class="isMinimized ? 'icon-music' : 'icon-close'"></i>
</div>
</div>
<!-- APlayer 播放器 -->
<div class="aplayer-container" v-show="!isMinimized" ref="playerRef"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import APlayer from 'aplayer';
import 'aplayer/dist/APlayer.min.css';
// 配置项
const props = defineProps({
playlistId: {
type: String,
default: '374466671'
},
server: {
type: String,
default: 'netease'
},
type: {
type: String,
default: 'playlist'
},
autoplay: {
type: Boolean,
default: false
},
theme: {
type: String,
default: '#b7daff'
},
defaultShow: {
type: Boolean,
default: true
},
metingApi: {
type: String,
default: 'https://api.injahow.cn/meting/'
}
});
const playerRef = ref<HTMLElement | null>(null);
const isMinimized = ref(false);
const isShow = ref(props.defaultShow);
let player: APlayer | null = null;
// 拖拽相关状态
const isDragging = ref(false);
const playerPosition = ref({
right: '20px',
bottom: '20px',
left: 'auto',
top: 'auto'
});
const dragOffset = ref({ x: 0, y: 0 });
// 初始化播放器
const initPlayer = async () => {
if (!playerRef.value) return;
try {
const metingApiUrl = \`\${props.metingApi}?server=\${props.server}&type=\${props.type}&id=\${props.playlistId}\`;
console.log('正在加载音乐列表...');
const response = await fetch(metingApiUrl);
if (!response.ok) {
throw new Error(\`API 请求失败: \${response.status}\`);
}
const musicList = await response.json();
if (!Array.isArray(musicList) || musicList.length === 0) {
throw new Error('歌单为空或格式错误');
}
console.log(\`成功加载 \${musicList.length} 首歌曲\`);
// 初始化 APlayer
player = new APlayer({
container: playerRef.value,
audio: musicList,
autoplay: props.autoplay,
theme: props.theme,
loop: 'all',
order: 'list',
preload: 'auto',
volume: 0.7,
mutex: true,
listFolded: false,
listMaxHeight: '250px',
lrcType: 3,
storageName: 'vuepress-music-player'
});
// 保存播放器状态
player.on('play', () => {
localStorage.setItem('music-player-playing', 'true');
});
player.on('pause', () => {
localStorage.setItem('music-player-playing', 'false');
});
console.log('音乐播放器初始化成功!');
} catch (error) {
console.error('音乐播放器初始化失败:', error);
}
};
// 切换播放器显示状态
const togglePlayer = () => {
isMinimized.value = !isMinimized.value;
localStorage.setItem('music-player-minimized', isMinimized.value.toString());
};
// 开始拖拽
const startDrag = (e: MouseEvent | TouchEvent) => {
const target = e.target as HTMLElement;
// 如果是收起状态,允许在整个播放器上拖拽
if (isMinimized.value) {
if (target.closest('.player-toggle') || target.closest('.aplayer')) {
return;
}
} else {
// 展开状态,只允许在头部拖拽
if (!target.closest('.player-header') && !target.closest('.drag-handle')) {
return;
}
}
isDragging.value = true;
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
dragOffset.value = {
x: clientX,
y: clientY
};
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchmove', onDrag);
document.addEventListener('touchend', stopDrag);
e.preventDefault();
};
// 拖拽中
const onDrag = (e: MouseEvent | TouchEvent) => {
if (!isDragging.value) return;
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
const deltaX = clientX - dragOffset.value.x;
const deltaY = clientY - dragOffset.value.y;
const wrapper = document.querySelector('.music-player-wrapper') as HTMLElement;
if (wrapper) {
const rect = wrapper.getBoundingClientRect();
const newLeft = rect.left + deltaX;
const newTop = rect.top + deltaY;
// 限制在视口范围内
const maxX = window.innerWidth - rect.width;
const maxY = window.innerHeight - rect.height;
const constrainedLeft = Math.max(0, Math.min(newLeft, maxX));
const constrainedTop = Math.max(0, Math.min(newTop, maxY));
playerPosition.value = {
left: \`\${constrainedLeft}px\`,
top: \`\${constrainedTop}px\`,
right: 'auto',
bottom: 'auto'
};
dragOffset.value = {
x: clientX,
y: clientY
};
}
};
// 停止拖拽
const stopDrag = () => {
if (isDragging.value) {
isDragging.value = false;
// 保存位置到 localStorage
localStorage.setItem('music-player-position', JSON.stringify(playerPosition.value));
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', onDrag);
document.removeEventListener('touchend', stopDrag);
}
};
// 恢复保存的位置
const restorePosition = () => {
try {
const savedPosition = localStorage.getItem('music-player-position');
if (savedPosition) {
playerPosition.value = JSON.parse(savedPosition);
}
} catch (error) {
console.error('恢复播放器位置失败:', error);
}
};
// 组件挂载
onMounted(() => {
const savedMinimized = localStorage.getItem('music-player-minimized');
if (savedMinimized !== null) {
isMinimized.value = savedMinimized === 'true';
}
restorePosition();
initPlayer();
});
// 组件卸载
onBeforeUnmount(() => {
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', onDrag);
document.removeEventListener('touchend', stopDrag);
if (player) {
player.destroy();
}
});
</script>
<style scoped lang="scss">
.music-player-wrapper {
position: fixed;
z-index: 9999;
user-select: none;
.music-player-container {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
transition: box-shadow 0.3s ease;
&.dragging {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
cursor: move;
cursor: grab;
cursor: -webkit-grab;
}
&.minimized {
width: 60px;
height: 60px;
cursor: move;
cursor: grab;
&:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.aplayer-container {
display: none;
}
.player-header {
display: none;
}
.player-toggle {
position: relative;
top: 0;
right: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
pointer-events: none;
&:hover {
transform: scale(1.05);
}
i {
font-size: 24px;
color: #fff;
pointer-events: auto;
}
}
}
// 头部区域(拖拽手柄 + 按钮)
.player-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: move;
cursor: grab;
&:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.drag-handle {
display: flex;
align-items: center;
gap: 6px;
color: #fff;
font-size: 14px;
flex: 1;
i {
font-size: 16px;
}
.drag-text {
font-weight: 500;
opacity: 0.9;
}
}
.player-toggle {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
i {
font-size: 14px;
color: #fff;
}
}
}
.aplayer-container {
width: 350px;
:deep(.aplayer) {
border-radius: 0 0 12px 12px;
box-shadow: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
.aplayer-list {
max-height: 250px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
&:hover {
background: #bbb;
}
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.music-player-wrapper {
.music-player-container {
&:not(.minimized) .aplayer-container {
width: calc(100vw - 40px);
max-width: 350px;
}
.player-header .drag-text {
display: none;
}
}
}
}
// 暗黑模式适配
@media (prefers-color-scheme: dark) {
.music-player-container {
background: #1e1e1e;
.player-header {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
}
}
}
</style>
核心功能解析
拖拽实现原理
// 1. 监听鼠标/触摸事件
@mousedown="startDrag"
@touchstart="startDrag"
// 2. 计算拖拽偏移量
const deltaX = clientX - dragOffset.value.x;
const deltaY = clientY - dragOffset.value.y;
// 3. 更新位置(限制在视口内)
const constrainedLeft = Math.max(0, Math.min(newLeft, maxX));
const constrainedTop = Math.max(0, Math.min(newTop, maxY));
// 4. 保存位置到 localStorage
localStorage.setItem('music-player-position', JSON.stringify(playerPosition.value));
状态持久化
// 保存状态
localStorage.setItem('music-player-minimized', isMinimized.value.toString());
localStorage.setItem('music-player-position', JSON.stringify(playerPosition.value));
// 恢复状态
onMounted(() => {
restorePosition();
initPlayer();
});
2. 创建全局包装组件
文件路径: src/.vuepress/components/GlobalMusicPlayer.vue
<template>
<ClientOnly>
<MusicPlayer
v-if="config.enable"
:playlistId="config.playlistId"
:server="config.server"
:type="config.type"
:autoplay="config.autoplay"
:theme="config.theme"
:defaultShow="config.defaultShow"
:metingApi="config.metingApi"
/>
</ClientOnly>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import MusicPlayer from './MusicPlayer.vue';
// 默认配置
const config = ref({
enable: true,
playlistId: '374466671', // 你的网易云音乐歌单ID
server: 'netease',
type: 'playlist',
autoplay: false,
theme: '#b7daff',
defaultShow: true,
metingApi: 'https://api.injahow.cn/meting/'
});
onMounted(() => {
try {
const savedConfig = localStorage.getItem('music-player-config');
if (savedConfig) {
config.value = { ...config.value, ...JSON.parse(savedConfig) };
}
} catch (error) {
console.error('读取音乐播放器配置失败:', error);
}
});
</script>
3. 注册全局组件
文件路径: src/.vuepress/client.ts
在文件顶部添加导入:
import GlobalMusicPlayer from './components/GlobalMusicPlayer.vue';
在 defineClientConfig 中添加:
export default defineClientConfig({
enhance: ({ app, router, siteData }) => {
// ... 其他代码
},
rootComponents: [GlobalMusicPlayer], // 添加这一行
});
完整示例:
import { defineClientConfig } from "@vuepress/client";
import GlobalMusicPlayer from './components/GlobalMusicPlayer.vue';
export default defineClientConfig({
enhance: ({ app, router, siteData }) => {
// 你的其他配置
},
rootComponents: [GlobalMusicPlayer], // 添加全局音乐播放器
});
⚙️ 配置说明
基础配置
修改 GlobalMusicPlayer.vue 中的配置:
const config = ref({
enable: true, // 是否启用
playlistId: '374466671', // 歌单ID
server: 'netease', // 平台
type: 'playlist', // 类型
autoplay: false, // 自动播放
theme: '#b7daff', // 主题色
defaultShow: true, // 默认显示
metingApi: 'https://api.injahow.cn/meting/' // API地址
});
配置项详解
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enable | Boolean | true | 是否启用音乐播放器 |
playlistId | String | '374466671' | 歌单/歌曲ID |
server | String | 'netease' | 音乐平台:netease(网易云)、tencent(QQ音乐)、kugou(酷狗) |
type | String | 'playlist' | 类型:song(单曲)、playlist(歌单)、album(专辑)、artist(歌手) |
autoplay | Boolean | false | 是否自动播放(注意浏览器限制) |
theme | String | '#b7daff' | 播放器主题颜色(支持任何CSS颜色值) |
defaultShow | Boolean | true | 是否默认显示播放器 |
metingApi | String | 'https://...' | Meting API 地址 |
📖 使用方法
1. 获取网易云音乐歌单ID
方法一:网页版
- 打开网易云音乐:https://music.163.com/
- 登录你的账号
- 找到或创建你的歌单
- 点击进入歌单页面
- 查看地址栏
示例地址:
https://music.163.com/#/playlist?id=374466671
^^^^^^^^^
这就是歌单ID
方法二:移动端分享
- 打开网易云音乐 APP
- 找到歌单,点击分享
- 复制链接
- 从链接中提取
id=后面的数字
2. 修改配置
编辑 src/.vuepress/components/GlobalMusicPlayer.vue:
const config = ref({
enable: true,
playlistId: '你的歌单ID', // 👈 修改这里
server: 'netease',
// ... 其他配置
});
3. 启动项目
npm run docs:dev
4. 使用播放器
- 拖拽移动:鼠标按住头部区域(或收起状态下的整个图标)拖动
- 展开/收起:点击右上角的按钮
- 播放控制:使用播放器内置的控制按钮
- 位置重置:如果播放器位置不合适,可以清除浏览器 localStorage
🎨 进阶优化
1. 自定义主题色
支持多种主题色方案:
// 淡蓝色(默认)
theme: '#b7daff'
// 渐变紫色
theme: '#764ba2'
// 粉色
theme: '#FF6B9D'
// 橙色
theme: '#FF8C3D'
// 绿色
theme: '#52c41a'
2. 播放不同类型
播放单曲
{
playlistId: '歌曲ID',
server: 'netease',
type: 'song'
}
播放专辑
{
playlistId: '专辑ID',
server: 'netease',
type: 'album'
}
播放歌手
{
playlistId: '歌手ID',
server: 'netease',
type: 'artist'
}
播放QQ音乐
{
playlistId: 'QQ音乐歌单ID',
server: 'tencent',
type: 'playlist'
}
3. 自定义样式
修改 MusicPlayer.vue 中的样式:
// 修改播放器宽度
.aplayer-container {
width: 400px; // 默认 350px
}
// 修改圆角
.music-player-container {
border-radius: 16px; // 默认 12px
}
// 修改头部背景
.player-header {
background: linear-gradient(135deg, #your-color1 0%, #your-color2 100%);
}
4. 自建 Meting API
如果默认 API 不稳定,推荐自建:
使用 Docker 部署
# 1. 拉取镜像
docker pull injahow/meting-api
# 2. 运行容器
docker run -d \
--name meting-api \
-p 3000:3000 \
injahow/meting-api
# 3. 测试 API
curl http://localhost:3000/?server=netease&type=playlist&id=374466671
修改配置
{
metingApi: 'http://你的服务器IP:3000/'
}
参考资源
- Meting API 项目:https://github.com/injahow/meting-api
- Docker Hub:https://hub.docker.com/r/injahow/meting-api
❓ 常见问题
1. 播放器不显示?
排查步骤:
- 检查浏览器控制台是否有错误
- 确认
enable: true - 检查组件是否正确注册到
client.ts - 清除浏览器缓存:
Ctrl + Shift + Delete - 重启开发服务器
检查命令:
# 清理缓存
npm run docs:clean-dev
# 重新启动
npm run docs:dev
2. 歌单加载失败?
可能原因:
- ❌ 歌单ID错误
- ❌ 歌单设置为私密
- ❌ 网络问题
- ❌ API 不可用
解决方案:
// 1. 确认歌单ID正确
// 正确格式:纯数字,如 '374466671'
// 2. 确保歌单是公开的
// 在网易云音乐中设置歌单为公开
// 3. 测试 API 是否可用
fetch('https://api.injahow.cn/meting/?server=netease&type=playlist&id=374466671')
.then(res => res.json())
.then(data => console.log(data));
// 4. 尝试其他歌单
playlistId: '60198' // 网易云热歌榜
3. 拖拽不流畅?
优化建议:
// 添加硬件加速
.music-player-wrapper {
transform: translateZ(0);
will-change: transform;
}
// 优化过渡动画
.music-player-container {
transition: none; // 拖拽时禁用过渡
}
4. 歌曲无法播放?
常见原因:
- 版权限制 - 某些歌曲有地区限制
- VIP 歌曲 - 需要会员才能播放
- 下架歌曲 - 歌曲已从平台移除
解决方案:
- 更换其他歌曲
- 使用无版权限制的歌单
- 尝试其他音乐平台
5. 移动端显示异常?
检查响应式样式:
@media (max-width: 768px) {
.aplayer-container {
width: calc(100vw - 40px) !important;
max-width: 350px;
}
}
6. 播放器位置异常?
重置位置:
// 在浏览器控制台执行
localStorage.removeItem('music-player-position');
localStorage.removeItem('music-player-minimized');
// 然后刷新页面
location.reload();
7. 构建后不显示?
检查构建配置:
// vite.config.ts 或 config.ts
export default {
build: {
// 确保不忽略音乐播放器资源
rollupOptions: {
external: [],
}
}
}
📚 技术细节
APlayer API 配置
new APlayer({
container: playerRef.value, // 容器元素
audio: musicList, // 音频列表
autoplay: false, // 自动播放
theme: '#b7daff', // 主题色
loop: 'all', // 循环模式:all/one/none
order: 'list', // 播放顺序:list/random
preload: 'auto', // 预加载:auto/metadata/none
volume: 0.7, // 音量:0-1
mutex: true, // 互斥播放
listFolded: false, // 列表默认折叠
listMaxHeight: '250px', // 列表最大高度
lrcType: 3, // 歌词类型
storageName: 'aplayer' // localStorage 键名
});
Meting API 参数
https://api.injahow.cn/meting/?server={server}&type={type}&id={id}
| 参数 | 说明 | 可选值 |
|---|---|---|
server | 音乐平台 | netease, tencent, kugou, xiami |
type | 类型 | song, playlist, album, artist |
id | ID | 歌曲/歌单/专辑/歌手 ID |
返回格式:
[
{
"name": "歌曲名",
"artist": "歌手",
"url": "音频地址",
"cover": "封面地址",
"lrc": "歌词地址"
}
]
🔗 相关资源
官方文档
- APlayer: https://aplayer.js.org/
- Meting: https://github.com/metowolf/Meting
- VuePress 2: https://v2.vuepress.vuejs.org/
- vuepress-theme-hope: https://theme-hope.vuejs.press/
在线工具
- 网易云音乐: https://music.163.com/
- QQ音乐: https://y.qq.com/
- Meting API: https://api.injahow.cn/
社区资源
- GitHub Issues: 遇到问题可以提 Issue
- VuePress Discord: 加入社区讨论
- Stack Overflow: 搜索相关问题
📝 更新日志
v1.0.0 (2025-10-31)
- ✅ 初始版本发布
- ✅ 实现基础播放功能
- ✅ 添加拖拽功能
- ✅ 支持位置记忆
- ✅ 适配响应式设计
- ✅ 支持暗黑模式
待实现功能
- ⏳ 播放器设置面板
- ⏳ 多歌单快速切换
- ⏳ 歌曲搜索功能
- ⏳ 快捷键支持
- ⏳ 歌词翻译
- ⏳ 播放历史记录
💡 最佳实践
1. 性能优化
<!-- 使用 v-show 而不是 v-if -->
<div v-show="!isMinimized">...</div>
<!-- 懒加载播放器 -->
<ClientOnly>
<MusicPlayer />
</ClientOnly>
2. 用户体验
- 默认不自动播放(避免打扰用户)
- 记住用户的播放位置和状态
- 提供清晰的拖拽提示
- 响应式设计,适配所有设备
3. 错误处理
try {
await initPlayer();
} catch (error) {
console.error('初始化失败:', error);
// 显示友好的错误提示
}
4. 代码组织
- 组件职责单一
- 配置与逻辑分离
- 使用 TypeScript 提供类型安全
🎯 总结
通过本文档,您应该能够:
✅ 在 VuePress 项目中集成音乐播放器
✅ 实现拖拽、状态持久化等高级功能
✅ 自定义播放器样式和配置
✅ 处理常见问题和错误
✅ 进行性能优化和功能扩展
核心要点:
- 使用 APlayer + Meting 实现音乐播放
- 通过 mousedown/touchstart 实现拖拽
- 使用 localStorage 实现状态持久化
- 注意响应式设计和浏览器兼容性
- 处理好错误情况和边界场景
📞 技术支持
如有问题,欢迎反馈:
- 项目地址: [您的 Git 仓库]
- 文档位置:
src/notes/other/个人知识库/VuePress/ - 更新时间: 2025年10月31日
🎵 享受您的编程音乐时光!