LOADING

加载过慢请开启缓存 浏览器默认开启

给 ParticleX 主题添加深色模式和目录导航

最近给博客的 ParticleX 主题手动实现了深色模式切换和文章目录导航,记录一下思路和改动细节。

深色模式

原理

核心思路来自 TailwindCSS 的深色模式方案:

  • document.documentElement.classList<html> 标签上添加/移除 dark
  • CSS 里用 :root.dark 选择器定义深色下的颜色变量
  • localStorage 存储用户的主题偏好,支持三种状态:auto(跟随系统)、lightdark
  • window.matchMedia("(prefers-color-scheme: dark)").matches 检测系统主题

主题切换顺序是:auto → light → dark → auto,循环切换。

改动的文件

themes/particlex/_config.yml

给 highlight 加上深色主题配置:

highlight:
    enable: true
    style: github
    styleDark: github-dark

themes/particlex/layout/import.ejs

引入深色代码高亮样式,默认 disabled,深色模式时由 JS 启用:

<link
    rel="stylesheet"
    href=".../styles/<%= theme.highlight.style %>.min.css"
/>
<link
    rel="stylesheet"
    id="highlight-style-dark"
    disabled
    href=".../styles/<%= theme.highlight.styleDark || 'github-dark' %>.min.css"
/>

themes/particlex/layout/layout.ejs

<head> 里加防白屏闪烁脚本,页面渲染前就先判断主题并加上 dark 类,避免刷新时出现短暂白屏:

<script>
    if (localStorage.getItem('theme') === 'dark' ||
        (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
    ) {
        document.documentElement.classList.add('dark');
    }
</script>

这段脚本必须放在 <head> 里、CSS 加载之前执行,否则还是会闪。

themes/particlex/layout/menu.ejs

在导航栏右侧加切换按钮,图标根据当前主题状态动态切换:

<button id="theme-toggle" @click="handleThemeSwitch" :title="theme">
    <i class="fa-solid"
       :class="theme === 'dark' ? 'fa-sun' : theme === 'light' ? 'fa-moon' : 'fa-circle-half-stroke'">
    </i>
</button>

三种图标对应三种状态:

  • fa-circle-half-stroke → auto(跟随系统)
  • fa-moon → light(浅色,点击切深色)
  • fa-sun → dark(深色,点击切回 auto)

themes/particlex/source/js/main.js

这里的逻辑其实主题本身已经写好了,主要包括:

// 初始化时读取 localStorage
theme: localStorage.getItem("theme") || "auto",

// 检测系统深色模式
isSystemDarkMode() {
    return window.matchMedia("(prefers-color-scheme: dark)").matches;
},

// 设置深色/浅色
setDarkMode(dark) {
    if (dark) {
        document.documentElement.classList.add("dark");
        document.getElementById("highlight-style-dark").removeAttribute("disabled");
    } else {
        document.documentElement.classList.remove("dark");
        document.getElementById("highlight-style-dark").setAttribute("disabled", "");
    }
},

// 点击按钮循环切换
handleThemeSwitch() {
    this.theme = ((theme) => {
        switch (theme) {
            case "auto":  this.setDarkMode(false); return "light";
            case "light": this.setDarkMode(true);  return "dark";
            case "dark":
                this.isSystemDarkMode() ? this.setDarkMode(true) : this.setDarkMode(false);
                return "auto";
        }
    })(this.theme)
},

页面卸载前把主题偏好写回 localStorageauto 状态则清除,下次打开重新跟随系统:

window.addEventListener("beforeunload", () => {
    if (this.theme === "auto")
        localStorage.removeItem("theme");
    else
        localStorage.setItem("theme", this.theme);
});

目录导航(TOC)

原理

toc.js 里用 Vue 组件实现,挂载后扫描 id="main-content" 容器内所有 H1~H6 标签,自动生成目录列表。滚动时监听每个标题距页面顶部的距离,高亮当前可见的标题。

目录固定在页面右侧(position: fixed),宽屏(>1300px)显示,窄屏自动隐藏。

改动的文件

themes/particlex/layout/import.ejs

在 post 页面加载 toc.js

<% if (type === "post") { %>
<script src="<%- url_for("/js/lib/toc.js") %>"></script>
<% } %>

themes/particlex/layout/post.ejs

给文章内容容器加上 id="main-content",这是 toc.js 扫描标题的入口:

<div class="content" id="main-content" v-pre>
    <%- page.content %>
</div>

然后在文章内容后面放 <toc-component> 标签:

<toc-component></toc-component>

Vue 会把它渲染成右侧固定的目录面板。

使用

写文章时正常用 Markdown 标题语法,目录会自动生成:

## 一级章节
### 子章节
#### 更深的层级

目录最多显示 15 个字符,超出会截断加 ...