想在博客首页增加音乐播放功能,找了网上的各种方案,有的需要授权,有的不能和主题完美搭配,正好安装了腾讯的qclaw小龙虾,就让它打造一个侧边栏音乐播放器,思路是用最简单的代码来实现,因为本人收藏了大量无损音乐,所以音乐源也不采用其它网站的音乐,自己上传到云存储来实现。经过几次修改,现在还算初步满意了。
效果见本博客首页
具体操作是在主题的sidebar.php侧边栏文件中,增加一个调用选项
<?php require_once 'widgets/widget-musicplayer.php'; ?> <!-- 音乐播放 -->
新建一个播放器文件widget-musicplayer.php和歌单文件music-list.json。以后直接编辑歌单文件增加歌曲就行了。
播放器主文件widget-musicplayer.php
<?php
/**
* 迷你音乐播放器(跨页面续播版)
*
* 支持多页面之间保存播放状态,切换页面后自动恢复进度。
*/
if ( ! defined('__TYPECHO_ROOT_DIR__')) exit;
$musicFile = __DIR__ . '/music-list.json';
if (!file_exists($musicFile)) {
echo '<div class="hh-widget mt-3">歌单文件不存在,请创建 music-list.json</div>';
return;
}
$raw = file_get_contents($musicFile);
if ($raw === false) {
echo '<div class="hh-widget mt-3">无法读取歌单文件</div>';
return;
}
$musicList = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo '<div class="hh-widget mt-3">歌单解析失败:' . htmlspecialchars(json_last_error_msg()) . '</div>';
return;
}
if (empty($musicList) || !is_array($musicList)) {
echo '<div class="hh-widget mt-3">歌单为空或格式错误,请检查 music-list.json</div>';
return;
}
$filteredSongs = array_filter($musicList, function($song) {
return isset($song['t'], $song['f']) && is_string($song['t']) && is_string($song['f']);
});
$filteredSongs = array_values($filteredSongs);
if (empty($filteredSongs)) {
echo '<div class="hh-widget mt-3">歌单中没有有效的歌曲(需要包含 t 和 f 字段)</div>';
return;
}
$safeSongs = array_map(function($song) {
return [
't' => htmlspecialchars($song['t'], ENT_QUOTES, 'UTF-8'),
'f' => $song['f']
];
}, $filteredSongs);
?>
<div class="hh-widget mt-3">
<div class="widget-title p-2 w-100">
<div class="widget-title-top-bg" style="background:linear-gradient(45deg, #66bb6a, rgba(255,255,255,0.2));"></div>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" style="display:inline-block;vertical-align:middle;margin-right:4px;"><path d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>清韵悠扬播放器
</div>
<div class="widget-content p-2">
<div id="mini-player" class="mini-player">
<div class="mini-player-now" id="miniNow">加载中...</div>
<div class="mini-player-progress" id="miniProgress">
<div class="mini-player-progress-bar" id="miniProgressBar"></div>
</div>
<div class="mini-player-time">
<span id="miniCurrentTime">00:00</span><span id="miniTotalTime">00:00</span>
</div>
<div class="mini-player-controls">
<button class="mini-player-btn" id="miniPrev" title="上一首"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg></button>
<button class="mini-player-btn mini-player-btn-play" id="miniPlay" title="播放"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button>
<button class="mini-player-btn" id="miniNext" title="下一首"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
<button class="mini-player-btn mini-player-shuffle" id="miniShuffle" title="随机播放"><span class="mini-player-shuffle-label">顺序</span></button>
</div>
<div class="mini-player-list" id="miniList"></div>
</div>
<audio id="miniAudio" preload="metadata"></audio>
</div>
</div>
<style>
/* 原有样式保持不变,此处省略以节省篇幅,请复制您原来的样式 */
.mini-player{font-size:.85rem;color:var(--font-color-main)}
.mini-player-now{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:.4rem;font-size:.9rem}
.mini-player-progress{width:100%;height:4px;background-color:var(--overlay-color-dark-1);border-radius:2px;cursor:pointer;position:relative}
.mini-player-progress-bar{height:100%;width:0;background:linear-gradient(90deg,#66bb6a,#43a047);border-radius:2px;transition:width .15s linear}
.mini-player-time{display:flex;justify-content:space-between;font-size:.7rem;color:var(--font-color-main-light);margin:2px 0 .4rem}
.mini-player-controls{display:flex;justify-content:center;align-items:center;gap:1rem;margin-bottom:.5rem}
.mini-player-btn{background:none;border:none;cursor:pointer;color:var(--font-color-main);padding:4px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s ease}
.mini-player-btn:hover{background-color:var(--overlay-color-dark-1)}
.mini-player-btn-play{width:25px;height:25px;background-color:#43a047;color:var(--white)}
.mini-player-btn-play:hover{background-color:#388e3c}
.mini-player-shuffle{padding:2px 12px;border-radius:16px;background-color:var(--overlay-color-dark-1);font-size:.7rem;color:var(--font-color-main-light);transition:all .2s ease}
.mini-player-shuffle:hover{background-color:rgba(0,0,0,.12)}
.mini-player-shuffle.active{background-color:#43a047;color:var(--white)}
.mini-player-shuffle.active:hover{background-color:#388e3c}
.mini-player-shuffle-label{font-size:.7rem}
.mini-player-list{height:10rem;overflow-y:auto;border-top:1px solid var(--border-color-main,rgba(0,0,0,.08));padding-top:.3rem}
.mini-player-list::-webkit-scrollbar{width:2px!important;-webkit-appearance:none;appearance:none}
.mini-player-list::-webkit-scrollbar-track{background-color:transparent}
.mini-player-song{padding:.3rem .4rem;border-radius:4px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.8rem;color:var(--font-color-main-light);transition:all .2s ease}
.mini-player-song:hover{background-color:rgba(255,152,0,.1);color:#ff9800}
.mini-player-song.playing{color:#43a047;font-weight:600;background-color:rgba(67,160,71,.08)}
.mini-player-song.playing:hover{background-color:rgba(255,152,0,.1);color:#ff9800}
/* 手机版悬浮(可选) */
@media (max-width: 768px) {
.hh-widget.mt-3 { position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999; background: var(--background-color, #fff); box-shadow: 0 -2px 10px rgba(0,0,0,0.1); margin: 0 !important; border-radius: 12px 12px 0 0; padding: 8px 12px; }
.mini-player-list { max-height: 40vh; }
}
</style>
<script>
(function(){
var songs = <?php echo json_encode($safeSongs, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG); ?>;
var STORAGE_KEY = 'mini_player_state';
// 读取保存的状态
var savedState = null;
try {
var rawState = sessionStorage.getItem(STORAGE_KEY);
if (rawState) savedState = JSON.parse(rawState);
} catch(e) { console.warn(e); }
var currentIdx = (savedState && typeof savedState.idx === 'number' && savedState.idx >= 0 && savedState.idx < songs.length) ? savedState.idx : Math.floor(Math.random() * songs.length);
var shuffle = (savedState && typeof savedState.shuffle === 'boolean') ? savedState.shuffle : true;
var savedCurrentTime = (savedState && typeof savedState.currentTime === 'number') ? savedState.currentTime : 0;
var wasPlaying = (savedState && savedState.playing === true) ? true : false;
var audio = document.getElementById('miniAudio');
var now = document.getElementById('miniNow');
var bar = document.getElementById('miniProgressBar');
var prog = document.getElementById('miniProgress');
var curT = document.getElementById('miniCurrentTime');
var totT = document.getElementById('miniTotalTime');
var list = document.getElementById('miniList');
var playBtn = document.getElementById('miniPlay');
var shuffleBtn = document.getElementById('miniShuffle');
var shuffleLabel = shuffleBtn.querySelector('.mini-player-shuffle-label');
var playHistory = [];
var unplayedQueue = [];
function initShuffleQueue() {
if (!shuffle) return;
var otherIndices = songs.map(function(_, i) { return i; }).filter(function(i) { return i !== currentIdx; });
for (var i = otherIndices.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = otherIndices[i];
otherIndices[i] = otherIndices[j];
otherIndices[j] = temp;
}
unplayedQueue = otherIndices;
}
function getNextIndex() {
if (songs.length === 1) return 0;
if (shuffle) {
if (unplayedQueue.length === 0) initShuffleQueue();
var next = unplayedQueue.shift();
playHistory.push(currentIdx);
return next;
} else {
playHistory.push(currentIdx);
return (currentIdx + 1) % songs.length;
}
}
function getPrevIndex() {
if (songs.length === 1) return 0;
if (playHistory.length === 0) return currentIdx;
return playHistory.pop();
}
songs.forEach(function(s, i){
var d = document.createElement('div');
d.className = 'mini-player-song';
d.textContent = (i+1) + '. ' + s.t;
d.onclick = function() { play(i); };
list.appendChild(d);
});
function fmt(t){
if (!t || !isFinite(t)) return '00:00';
var m = Math.floor(t / 60);
var s = Math.floor(t % 60);
return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
}
function highlight(){
var items = list.children;
for (var i = 0; i < items.length; i++) {
items[i].className = (i === currentIdx) ? 'mini-player-song playing' : 'mini-player-song';
}
setTimeout(function() {
var activeItem = list.children[currentIdx];
if (activeItem) activeItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, 50);
}
// 保存当前状态到 sessionStorage
function saveState() {
var state = {
idx: currentIdx,
shuffle: shuffle,
currentTime: audio.currentTime || 0,
playing: !audio.paused
};
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
// 页面关闭/刷新前保存
window.addEventListener('beforeunload', function() {
saveState();
});
// 播放/进度更新时自动保存(可选,提高实时性)
setInterval(function() {
if (audio.src && audio.currentTime > 0) saveState();
}, 5000);
function play(i, seekTime){
if (i < 0 || i >= songs.length) return;
var oldIdx = currentIdx;
currentIdx = i;
if (audio.src !== songs[i].f) {
audio.src = songs[i].f;
audio.load();
}
audio.play().catch(function(e){ console.warn('播放失败:', e); });
now.textContent = songs[i].t;
playBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>';
highlight();
if (seekTime !== undefined && !isNaN(seekTime) && seekTime > 0) {
audio.currentTime = seekTime;
}
if (shuffle) {
if (oldIdx !== currentIdx) playHistory.push(oldIdx);
var pos = unplayedQueue.indexOf(currentIdx);
if (pos !== -1) unplayedQueue.splice(pos, 1);
} else {
if (oldIdx !== currentIdx) playHistory.push(oldIdx);
}
saveState();
}
function updateShuffleUI(){
if (shuffle) {
shuffleBtn.classList.add('active');
shuffleLabel.textContent = '随机';
shuffleBtn.title = '随机播放中,点击切换为顺序播放';
playHistory = [];
initShuffleQueue();
} else {
shuffleBtn.classList.remove('active');
shuffleLabel.textContent = '顺序';
shuffleBtn.title = '顺序播放中,点击切换为随机播放';
playHistory = [];
unplayedQueue = [];
}
saveState();
}
// ---- 进度条拖拽 ----
var isDragging = false;
function setProgressFromEvent(e) {
if (!audio.duration) return;
var rect = prog.getBoundingClientRect();
var clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
var percent = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
audio.currentTime = percent * audio.duration;
bar.style.width = percent * 100 + '%';
saveState();
}
prog.addEventListener('mousedown', function(e) { isDragging = true; setProgressFromEvent(e); e.preventDefault(); });
window.addEventListener('mousemove', function(e) { if (isDragging) setProgressFromEvent(e); });
window.addEventListener('mouseup', function() { isDragging = false; });
prog.addEventListener('touchstart', function(e) { isDragging = true; setProgressFromEvent(e); e.preventDefault(); });
window.addEventListener('touchmove', function(e) { if (isDragging) setProgressFromEvent(e); });
window.addEventListener('touchend', function() { isDragging = false; });
prog.addEventListener('click', function(e) { setProgressFromEvent(e); });
audio.addEventListener('timeupdate', function(){
if (audio.duration && isFinite(audio.duration)) {
if (!isDragging) bar.style.width = (audio.currentTime / audio.duration * 100) + '%';
curT.textContent = fmt(audio.currentTime);
totT.textContent = fmt(audio.duration);
}
});
audio.addEventListener('ended', function(){
play(getNextIndex());
});
playBtn.onclick = function(){
if (!audio.src || audio.src === location.href) {
play(currentIdx, savedCurrentTime);
return;
}
if (audio.paused) {
audio.play().catch(function(e){ console.warn(e); });
playBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>';
} else {
audio.pause();
playBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
}
saveState();
};
document.getElementById('miniPrev').onclick = function(){ play(getPrevIndex()); };
document.getElementById('miniNext').onclick = function(){ play(getNextIndex()); };
shuffleBtn.onclick = function(){ shuffle = !shuffle; updateShuffleUI(); };
// 初始化
audio.src = songs[currentIdx].f;
now.textContent = songs[currentIdx].t;
highlight();
// 恢复进度
if (savedCurrentTime > 0) {
audio.currentTime = savedCurrentTime;
bar.style.width = (savedCurrentTime / (audio.duration || 1)) * 100 + '%';
}
initShuffleQueue();
updateShuffleUI();
// 如果之前是播放状态,尝试自动续播(受浏览器策略限制,可能失败)
if (wasPlaying) {
audio.play().catch(function(){
// 自动播放失败,显示提示,但保留进度
now.textContent = songs[currentIdx].t + '(点击播放续播)';
});
}
})();
</script>
歌单文件music-list.json
[
{"f": "https://***.com/muc/music/a01.mp3", "t": "JinriCP直播秀配乐一"},
{"f": "https://***.com/muc/music/a02.mp3", "t": "JinriCP直播秀配乐二"},
{"f": "https://***.com/muc/music/a03.mp3", "t": "Bressanone布列瑟农-马修连恩"}
]
更新:删除了自动播放改为手动,添加跨网页歌曲记忆功能 。
趣味性组件什么的,其实更偏向单独一个页面摆放会好很多。
这个跳转页面就没了,感觉聊胜于无
我觉得博客还是以内容为主比较好
确实