# Hugo LoveIt 主题：APlayer + PJAX 实现页面切换音乐不中断


## 引言

搭建个人博客后，一直想在左下角放个音乐播放器。切换页面时音乐不能断——这是最基本的要求。市面上大部分 Hugo 音乐播放器教程都针对 Stack 主题，LoveIt 的资料很少。折腾了好几天终于搞定了，记录一下。

## 最终效果

- 🎵 APlayer 完整 UI：封面 + 歌名/歌手 + 进度条 + 时间 + 音量 + 循环模式 + 歌单
- 🔀 PJAX 无刷新加载，切换页面音乐**零中断**
- 💾 localStorage 保进度，F5 刷新也能恢复
- 📝 `hugo.toml` 配置歌单，加歌不改代码

## 核心技术

[PJAX](https://github.com/MoOx/pjax) 拦截内部链接，用 AJAX 只替换变化的部分（`.main .container`）。APlayer 放在 `.wrapper` 外，永远不会被销毁。再配合 localStorage 保留播放进度，F5 后也能恢复。

## 文件改动

| 文件 | 作用 |
|------|------|
| `layouts/baseof.html` | 覆盖主题模板，加 APlayer + PJAX |
| `assets/js/theme.js` | 暴露 Theme 实例，加 `reinitContent()` |
| `hugo.toml` | `[[params.musicList]]` 歌单配置 |
| `static/music/` | 存放 mp3 和封面 jpg |

## 实现细节

### 1. baseof.html

从 `themes/LoveIt/layouts/baseof.html` 复制到项目同名路径，Hugo 会自动覆盖。

**APlayer**（放在 `.wrapper` 外）：

`listFolded: true` + 初始化后 JS 自动点击展开 → 切换到 `aplayer-withlist` 模式，全 UI 可见。

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css">
<div id="aplayer"></div>
<script src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
<script>
var ap = new APlayer({
  container: document.getElementById('aplayer'),
  fixed: true,
  theme: '#448aff',
  loop: 'all', order: 'list',
  volume: 0.7, mutex: true,
  lrcType: 0,
  listFolded: true,
  listMaxHeight: '250px',
  audio: [
    {{ range $index, $song := .Site.Params.musicList }}
    { name: '{{ $song.name }}', artist: '{{ $song.artist }}',
      url: '{{ $song.url }}', cover: '{{ $song.cover }}' },
    {{ end }}
  ]
});

// 自动展开切换到完整 UI 模式 + 自动打开歌单
setTimeout(function() {
  var btn = document.querySelector('#aplayer .aplayer-miniswitcher');
  if (btn) btn.click();
  setTimeout(function() {
    var listBtn = document.querySelector('#aplayer .aplayer-icon-menu');
    if (listBtn) listBtn.click();
  }, 200);
}, 500);

// 播放进度保留
window.onbeforeunload = function() {
  localStorage.setItem('playInfo', JSON.stringify({
    index: ap.list.index, currentTime: ap.audio.currentTime, paused: ap.paused
  }));
};
window.onload = function() {
  var saved = JSON.parse(localStorage.getItem('playInfo'));
  if (!saved) return;
  ap.list.switch(saved.index);
  setTimeout(function() { ap.seek(saved.currentTime); if (!saved.paused) ap.play(); }, 500);
};
</script>
```

少量 CSS 隐藏歌词、适配深色模式歌单背景：

```html
<style>
#aplayer .aplayer-lrc { display:none!important; }
[theme="dark"] #aplayer.aplayer-fixed .aplayer-list { background:#2e2e2e; }
[theme="dark"] #aplayer.aplayer-fixed .aplayer-list ol li { color:#ddd; border-top-color:rgba(255,255,255,0.07); }
</style>
```

**PJAX**（放在 `</body>` 前）：

```html
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>
<script>
var pjax = new Pjax({ selectors: [".main .container"] });

pjax._handleResponse = pjax.handleResponse;
pjax.handleResponse = function(responseText, request, href, options) {
  if (request.responseText.match("<html")) {
    if (responseText) {
      let newDom = new DOMParser().parseFromString(responseText, 'text/html');
      document.body.setAttribute("class", newDom.body.className);
      document.title = newDom.title;
      pjax._handleResponse(responseText, request, href, options);
    }
  }
};

document.addEventListener('pjax:complete', function() {
  window.reinitContent();
});
</script>
```

### 2. theme.js

从 `themes/LoveIt/assets/js/theme.js` 复制到 `assets/js/theme.js`，改末尾两处：

```js
const themeInit = () => {
  window.__theme = new Theme();
  window.__theme.init();
};

window.reinitContent = () => {
  if (window.__theme) {
    window.__theme.initRaw(); window.__theme.initSVGIcon();
    window.__theme.initHighlight(); window.__theme.initMath();
    window.__theme.initMermaid(); window.__theme.initHeaderLink();
    window.__theme.initDetails(); window.__theme.initLightGallery();
    window.__theme.initEcharts(); window.__theme.initMapbox();
    window.__theme.initTypeit(); window.__theme.initTwemoji();
    window.__theme.initCookieconsent();
    window.setTimeout(() => {
      window.__theme.initToc(); window.__theme.initComment();
    }, 100);
  }
};
```

> 菜单、搜索、主题切换在 header 里，PJAX 不替换 header，这些不需要重初始化。

### 3. hugo.toml

```toml
[[params.musicList]]
  name = "Coffee"
  artist = "beabadoobee"
  url = "/music/Coffee.mp3"
  cover = "/music/Coffee.jpg"
[[params.musicList]]
  name = "怪天气"
  artist = "YELLOW黃宣 / 9m88"
  url = "/music/怪天气.mp3"
  cover = "/music/怪天气.jpg"
```

每首歌一个 `[[params.musicList]]` 块，加歌只改这一个文件。音乐文件放在 `static/music/`。

## 窄模式陷阱

`listFolded: false` 会触发窄模式——**info 区（进度条、时间、音量）的 DOM 根本不创建**，后续改 CSS 没用。必须用 `listFolded: true` + JS 点击展开。

## PJAX 已知局限

| 问题 | 状态 |
|------|------|
| 切页后代码高亮丢失 | ✅ 已修复 |
| 切页后数学公式不渲染 | ✅ 已修复 |
| 切页后 TOC 消失 | ✅ 已修复 |
| 搜索跳转刷新页面 | ⚠️ 未完全解决 |

## 总结

1. **PJAX 只替换 `.main .container`**——不动 header 和 footer
2. **APlayer 在 `.wrapper` 外**——永远不被销毁
3. **`listFolded: true` + JS 展开**——利用原生机制，不跟 CSS 打架
4. **`hugo.toml` 配歌单**——模板语法读入，永远不改代码

> 感谢 [莱特雷-letere 的博客](https://letere-gzj.github.io/hugo-stack/p/hugo/custom-player/) 的思路。

