想在博客首页增加音乐播放功能,找了网上的各种方案,有的需要授权,有的不能和主题完美搭配,正好安装了腾讯的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布列瑟农-马修连恩"}
]

更新:删除了自动播放改为手动,添加跨网页歌曲记忆功能 。