Hexo+Orange主题下基于APlayer+MetingJs+pjax添加音乐播放器
2024-03-17 16:18:45

前言:

基于 Hexo 框架, Orange 主题,在页面右下角工具栏引入音乐播放器,播放网易云音乐在线个人歌单歌曲;

引入 pjax.js,实现切换页面,音乐播放器能继续播放音乐。

操作步骤:

1、在主题下的 layout/_partial 新增 music-player.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<% if (theme.music && theme.music.enable) { %>
<!-- iconfont 自己导入的 music 图标,先在https://www.iconfont.cn/中找到合适的 music 图标,将图标添加到项目,再引入项目在线链接 -->
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4973638_ovgeuq5516.css" />

<!-- 音乐播放器弹窗容器 -->
<div class="music-player-popup hidden" id="music-player-popup">
<div class="music-player-content" id="music-player-content">
<!-- 播放器内容将在点击时动态加载 -->
</div>
</div>

<!-- 音乐播放器配置 -->
<script>
window.musicPlayerConfig = {
server: "<%- theme.music.meting.server %>",
type: "<%- theme.music.meting.type %>",
id: "<%- theme.music.meting.id %>",
fixed: "<%- theme.music.meting.fixed %>",
mini: "<%- theme.music.meting.mini %>",
autoplay: "<%- theme.music.meting.autoplay %>",
theme: "<%- theme.music.meting.theme %>",
loop: "<%- theme.music.meting.loop %>",
order: "<%- theme.music.meting.order %>",
preload: "<%- theme.music.meting.preload %>",
volume: "<%- theme.music.meting.volume %>",
lrctype: "<%- theme.music.meting.lrctype || 0 %>",
listfolded: "<%- theme.music.meting.listfolded || true %>",
listmaxheight: "<%- theme.music.meting.listmaxheight || 0 %>"
};
</script>

<!-- 引入音乐播放器脚本 -->
<script src="/js/music-player.js"></script>

<!-- pjax -->
<% if (theme.cdns && theme.cdns.pjax && theme.cdns.pjax.enable) { %>
<script defer type="text/javascript" src="<%- theme.cdns.pjax.url %>"></script>
<% } else { %>
<script defer type="text/javascript" src="/plugins/jquery.pjax.min.js"></script>
<% } %>

<script src="/js/pjax.js"></script>

<% } %>

2、在主题下的 layout/layout.ejs 引入music-player.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="<%= config.language %>" color-mode="light">

<%- partial('_partial/header') %>

<body>
<div id="app">
<%- partial('_partial/navigation') %>

<div class="flex-container">
<%- body %>
<%- partial('_partial/footer') %>
</div>

<div class="tools-bar">
<%- partial('_partial/backtotop') %>

<%- partial('_partial/search') %>

<%- partial('_partial/colorscheme') %>

<!-- 音乐播放器图标 -->
<% if (theme.music && theme.music.enable) { %>
<div class="music-icon tools-bar-item" id="music-icon">
<a href="javascript: void(0)">
<i class="iconfont icon-music"></i>
</a>
</div>
<% } %>

<%- partial('_partial/shares') %>
</div>
</div>

<!-- 音乐播放器弹窗放在PJAX容器外部,保持播放状态 -->
<%- partial('_partial/music-player') %>
</body>
</html>

3、在主题下的 source/css 新增 music-player.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/* 简洁音乐播放器样式 */
.music-icon {
position: relative;
}

/* 音乐播放器弹窗 */
.music-player-popup {
position: fixed;
bottom: 110px;
right: 15px;
width: 280px;
max-width: calc(100vw - 30px);
background: var(--bg-body);
border: 1px solid var(--color-divider-md-border);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
z-index: 998;
opacity: 0;
transform: translateY(8px);
transition: all 0.2s ease-out;
}



.music-player-popup.hidden {
display: none;
}

.music-player-popup.show {
opacity: 1;
transform: translateY(0);
}

/* 播放器内容区域 */
.music-player-content {
padding: 7px;
background: var(--bg-body);
border-radius: 12px;
overflow: hidden;
}

/* MetingJS播放器样式 */
.music-player-content meting-js .aplayer {
background: var(--bg-body);
border-radius: 12px;
box-shadow: none;
margin: 0;
font-size: 13px;
border: none;
}

/* 调整播放器主体大小 */
.music-player-content meting-js .aplayer .aplayer-body {
padding: 1px;
}

.music-player-content meting-js .aplayer .aplayer-info {
padding: 2px;
height: 65px;
margin-left: 65px;
}

/* 封面图片样式*/
.music-player-content meting-js .aplayer .aplayer-pic {
width: 60px;
height: 60px;
border-radius: 12px;
}

/* 音乐信息区域 */
.music-player-content meting-js .aplayer .aplayer-info .aplayer-music {
padding-top: 5px;
}

.music-player-content meting-js .aplayer .aplayer-info .aplayer-music .aplayer-title {
font-size: 13px;
font-weight: 500;
line-height: 1.3;
margin-bottom: 2px;
}

.music-player-content meting-js .aplayer .aplayer-info .aplayer-music .aplayer-author {
font-size: 11px;
line-height: 1.2;
opacity: 0.7;
}

/* 控制器区域 */
.music-player-content meting-js .aplayer .aplayer-controller {
margin-top: 20px;
}

.music-player-content meting-js .aplayer .aplayer-controller .aplayer-play {
}

.music-player-content meting-js .aplayer .aplayer-controller .aplayer-bar-wrap {
}

.music-player-content meting-js .aplayer .aplayer-controller .aplayer-time {
}

/* 播放列表样式 - 允许切换显示/隐藏 */
.music-player-content meting-js .aplayer .aplayer-list {
max-height: 200px;
overflow-y: auto;
background: var(--bg-body);
border-radius: 0 0 12px 12px;
transition: max-height 0.3s ease;
}

/* 当播放列表隐藏时 */
.music-player-content meting-js .aplayer:not(.aplayer-withlist) .aplayer-list {
max-height: 0;
overflow: hidden;
}

/* 隐藏歌词相关 */
.music-player-content meting-js .aplayer .aplayer-lrc {
display: none !important;
}

.music-player-content meting-js .aplayer .aplayer-lrc-button {
display: none !important;
}

/* MetingJS容器样式 */
.music-player-content meting-js {
border-radius: 12px;
overflow: hidden;
display: block;
}

/* 响应式设计 */
@media (max-width: 768px) {
.music-player-popup {
width: 260px;
bottom: 80px;
right: 12px;
}
}

@media (max-width: 480px) {
.music-player-popup {
width: calc(100vw - 24px);
bottom: 75px;
right: 12px;
left: 12px;
}
}

@media (max-width: 360px) {
.music-player-popup {
width: calc(100vw - 20px);
bottom: 70px;
right: 10px;
left: 10px;
}
}

/* 深色模式适配 */
:root[color-mode="dark"] .music-player-popup {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: rgba(255, 255, 255, 0.1);
}

/* 滚动条样式 */
.music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar {
width: 3px;
}

.music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar-track {
background: transparent;
}

.music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}

.music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}

:root[color-mode="dark"] .music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}

:root[color-mode="dark"] .music-player-content meting-js .aplayer .aplayer-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}

4、在主题下的 source/js 新增 music-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 音乐播放器动态加载和控制
let musicPlayerLoaded = localStorage.getItem('musicPlayerLoaded') === 'true';
let musicIcon, musicPlayerPopup;

document.addEventListener('DOMContentLoaded', function() {
musicIcon = document.querySelector('#music-icon');
musicPlayerPopup = document.querySelector('#music-player-popup');

if (!musicIcon || !musicPlayerPopup) return;

// 如果之前已经加载过,直接初始化播放器
if (musicPlayerLoaded) {
initializeLoadedPlayer();
}

// 绑定事件
bindEvents();
});

// 绑定所有事件
function bindEvents() {
// 点击音乐图标
musicIcon.addEventListener('click', handleIconClick);

// 点击外部区域隐藏播放器
document.addEventListener('click', handleOutsideClick);

// 阻止弹窗内部点击事件冒泡
musicPlayerPopup.addEventListener('click', e => e.stopPropagation());

// ESC键关闭播放器
document.addEventListener('keydown', handleKeydown);
}

// 处理图标点击
function handleIconClick(e) {
e.preventDefault();
e.stopPropagation();

if (!musicPlayerLoaded) {
loadMusicPlayer();
} else {
toggleMusicPlayer();
}
}

// 处理外部点击
function handleOutsideClick(e) {
if (musicPlayerLoaded && !musicPlayerPopup.contains(e.target) && !musicIcon.contains(e.target)) {
hideMusicPlayer();
}
}

// 处理键盘事件
function handleKeydown(e) {
if (e.key === 'Escape' && musicPlayerLoaded && !musicPlayerPopup.classList.contains('hidden')) {
hideMusicPlayer();
}
}

// 动态加载音乐播放器
function loadMusicPlayer() {
// 加载资源
loadCSS('/css/music-player.css');
loadCSS('https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css');

// 依次加载JavaScript
loadScript('https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js')
.then(() => loadScript('https://cdn.jsdelivr.net/npm/meting@2/dist/Meting.min.js'))
.then(() => {
createMusicPlayer();
musicPlayerLoaded = true;
localStorage.setItem('musicPlayerLoaded', 'true');
showMusicPlayer();
})
.catch(error => console.error('音乐播放器加载失败:', error));
}

// 初始化已加载的播放器
function initializeLoadedPlayer() {
// 确保CSS已加载
loadCSS('/css/music-player.css');
loadCSS('https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css');

// 确保JS已加载
Promise.all([
ensureScriptLoaded('https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js'),
ensureScriptLoaded('https://cdn.jsdelivr.net/npm/meting@2/dist/Meting.min.js')
]).then(() => createMusicPlayer());
}

// 确保脚本已加载
function ensureScriptLoaded(src) {
return document.querySelector(`script[src="${src}"]`) ?
Promise.resolve() : loadScript(src);
}

// 创建MetingJS播放器
function createMusicPlayer() {
const content = document.getElementById('music-player-content');
const config = window.musicPlayerConfig || {};

content.innerHTML = `
<meting-js
server="${config.server || 'netease'}"
type="${config.type || 'playlist'}"
id="${config.id || '60198'}"
fixed="${config.fixed || 'false'}"
mini="${config.mini || 'false'}"
autoplay="${config.autoplay || 'false'}"
theme="${config.theme || '#808080'}"
loop="${config.loop || 'all'}"
order="${config.order || 'list'}"
preload="${config.preload || 'auto'}"
volume="${config.volume || '0.7'}"
lrc-type="${config.lrctype || '0'}"
list-folded="${config.listfolded || 'true'}"
list-max-height="${config.listmaxheight || '0'}">
</meting-js>
`;
}

// 加载CSS文件
function loadCSS(href) {
if (document.querySelector(`link[href="${href}"]`)) return;

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
}

// 加载JavaScript文件
function loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}

const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}

// 显示音乐播放器
function showMusicPlayer() {
musicPlayerPopup.classList.remove('hidden');
requestAnimationFrame(() => {
musicPlayerPopup.classList.add('show');
});
}

// 切换音乐播放器显示状态
function toggleMusicPlayer() {
const isHidden = musicPlayerPopup.classList.contains('hidden');
isHidden ? showMusicPlayer() : hideMusicPlayer();
}

// 隐藏音乐播放器
function hideMusicPlayer() {
musicPlayerPopup.classList.remove('show');
setTimeout(() => {
musicPlayerPopup.classList.add('hidden');
}, 200);
}

5、引入 jquery.pjax.min.js

​ 访问https://cdn.jsdelivr.net/npm/jquery-pjax@2.0.1/jquery.pjax.min.js,复制全部代码,在主题下的source/plugins 下新建jquery.pjax.min.js 文件,将复制的代码粘贴进去并保存。

6、在主题下的 source/js 新增 pjax.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// PJAX 无刷新页面切换
function initPJAX() {
// 检查依赖
if (typeof window.$ === 'undefined' || !window.$.fn.pjax) {
setTimeout(initPJAX, 1000);
return;
}

// 初始化PJAX
$(document).pjax('a[href^="/"]:not([href*="#"]):not([no-pjax]):not([target="_blank"])', '#app', {
fragment: '#app',
timeout: 8000,
});

$(document).on('pjax:end', rebindMusicIcon);
}


// 重新绑定音乐图标事件
function rebindMusicIcon() {
const musicIcon = document.querySelector('#music-icon');
const musicPlayerPopup = document.querySelector('#music-player-popup');

if (!musicIcon || !musicPlayerPopup) return;

// 移除旧的事件监听器
if (window.musicIconClickHandler) {
musicIcon.removeEventListener('click', window.musicIconClickHandler);
}

// 创建新的事件处理器
window.musicIconClickHandler = function(e) {
e.preventDefault();
e.stopPropagation();

if (localStorage.getItem('musicPlayerLoaded') === 'true') {
togglePlayerDisplay(musicPlayerPopup);
}
};

// 绑定新的事件监听器
musicIcon.addEventListener('click', window.musicIconClickHandler);
}

// 切换播放器显示状态
function togglePlayerDisplay(popup) {
const isHidden = popup.classList.contains('hidden');
if (isHidden) {
popup.classList.remove('hidden');
requestAnimationFrame(() => popup.classList.add('show'));
} else {
popup.classList.remove('show');
setTimeout(() => popup.classList.add('hidden'), 200);
}
}

// 多种方式初始化PJAX
['DOMContentLoaded', 'load'].forEach(event => {
document.addEventListener(event, () => setTimeout(initPJAX, event === 'load' ? 1000 : 2000));
});

if (document.readyState === 'complete') {
setTimeout(initPJAX, 500);
}

7、修改主题下的_config.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
cdns:
jquery:
enable: false
url: https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js
# 新增 pjax 配置
pjax:
enable: false
url: https://cdn.jsdelivr.net/npm/jquery-pjax@2.0.1/jquery.pjax.min.js


# 新增音乐播放器
music:
enable: true
# MetingJS配置
meting:
# 音乐平台:netease, tencent, kugou, xiami, baidu
server: netease
# 播放类型:song, playlist, album, search, artist
type: playlist
# 对应的歌单ID
id: 13970277026
# 是否固定模式
fixed: false
# 是否开启迷你模式
mini: false
# 是否自动播放
autoplay: false
# 主题色
theme: '#808080'
# 循环模式:all, one, none
loop: all
# 播放顺序:list, random
order: list
# 预加载:none, metadata, auto
preload: auto
# 音量:0-1
volume: 0.7
# 默认不显示歌词
lrctype: 0
# 默认展开播放列表
listfolded: false
# 播放列表最大高度
listmaxheight: 200

最终效果:

image-20240317201347516

相关知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//初始化PJAX:
//第一个参数为 CSS选择器,用于指定哪些链接会触发PJAX
//第二个参数为 容器选择器,指定PJAX更新的容器,即页面中哪个元素的内容会被替换。
//第三个参数为 配置选项,
// fragment: '#app' - 从服务器响应的HTML中提取#app元素的内容来替换当前页面的#app
// timeout: 8000 - 如果请求超过8秒未完成,则回退到普通页面跳转
//工作原理:
// 1、拦截点击:当用户点击符合条件的链接时,PJAX会拦截这个点击事件
// 2、AJAX请求:发送AJAX请求获取目标页面的HTML
// 3、从响应中提取#app部分,替换当前页面的#app内容
$(document).pjax('a[href^="/"]:not([href*="#"]):not([no-pjax]):not([target="_blank"])', '#app', {
fragment: '#app',
timeout: 8000,
});
//所以在 layout/layout.ejs 中,音乐播放器是放在<div id='#app'>元素外的。切换页面,pjax 每次都重新更新#app里的内容,然后在 pjax 结束时重新绑定音乐图标事件,实现音乐播放器不被刷新。
$(document).on('pjax:end', rebindMusicIcon);

TODO:

1、基本效果已实现,不过使用pjax后,切换页面时,控制台会输出报错:

image-20240317223156766

2、代码待优化,如脚本的引入可能存在重复或者放在了不合适的地方等问题。