最近给博客的 ParticleX 主题手动实现了深色模式切换和文章目录导航,记录一下思路和改动细节。
深色模式
原理
核心思路来自 TailwindCSS 的深色模式方案:
- 用
document.documentElement.classList在<html>标签上添加/移除dark类 - CSS 里用
:root.dark选择器定义深色下的颜色变量 - 用
localStorage存储用户的主题偏好,支持三种状态:auto(跟随系统)、light、dark - 用
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)
},
页面卸载前把主题偏好写回 localStorage,auto 状态则清除,下次打开重新跟随系统:
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 个字符,超出会截断加 ...。