feat(theme): 新增主题模式配置,支持跟随系统
- 新增 site.theme.mode 配置(dark/light/system) - 支持 prefers-color-scheme 系统主题跟随 - 用户手动切换后停止跟随并持久化 ref #36
This commit is contained in:
@@ -137,18 +137,23 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优
|
|||||||
- 可选 `fonts.preload: true`:用 `preload + onload` 的方式非阻塞加载外链字体 CSS(更利于首屏性能)
|
- 可选 `fonts.preload: true`:用 `preload + onload` 的方式非阻塞加载外链字体 CSS(更利于首屏性能)
|
||||||
- 首页副标题(渐变发光样式)使用全站基础字体(跟随 `fonts` 配置)
|
- 首页副标题(渐变发光样式)使用全站基础字体(跟随 `fonts` 配置)
|
||||||
|
|
||||||
5. **顶部欢迎信息与社交链接**
|
5. **主题(默认明暗模式)**
|
||||||
|
- `theme.mode: dark | light | system`
|
||||||
|
- `dark/light`:首屏默认主题;用户点击按钮切换后会写入 localStorage 并覆盖该默认值
|
||||||
|
- `system`:跟随系统 `prefers-color-scheme`;用户手动切换后同样会写入 localStorage 并停止跟随
|
||||||
|
|
||||||
|
6. **顶部欢迎信息与社交链接**
|
||||||
- `profile`:首页顶部欢迎信息
|
- `profile`:首页顶部欢迎信息
|
||||||
- `social`:侧边栏底部社交链接
|
- `social`:侧边栏底部社交链接
|
||||||
- `profile.title` / `profile.subtitle`:分别对应首页顶部主标题与副标题
|
- `profile.title` / `profile.subtitle`:分别对应首页顶部主标题与副标题
|
||||||
|
|
||||||
6. **导航**
|
7. **导航**
|
||||||
- `navigation[]`:页面入口列表,`id` 需唯一,并与 `pages/<id>.yml` 对应(例如 `id: common` 对应 `pages/common.yml`)
|
- `navigation[]`:页面入口列表,`id` 需唯一,并与 `pages/<id>.yml` 对应(例如 `id: common` 对应 `pages/common.yml`)
|
||||||
- 默认首页由 `navigation` 数组顺序决定:**第一项即为首页(默认打开页)**,不再使用 `active` 字段
|
- 默认首页由 `navigation` 数组顺序决定:**第一项即为首页(默认打开页)**,不再使用 `active` 字段
|
||||||
- 图标使用 Font Awesome 类名字符串(例如 `fas fa-home`、`fab fa-github`)
|
- 图标使用 Font Awesome 类名字符串(例如 `fas fa-home`、`fab fa-github`)
|
||||||
- 导航显示顺序与数组顺序一致,可通过调整数组顺序改变导航顺序
|
- 导航显示顺序与数组顺序一致,可通过调整数组顺序改变导航顺序
|
||||||
|
|
||||||
7. **RSS(articles Phase 2)**
|
8. **RSS(articles Phase 2)**
|
||||||
- `rss.*`:仅用于 `npm run sync-articles`(联网抓取 RSS/Atom 并写入缓存)
|
- `rss.*`:仅用于 `npm run sync-articles`(联网抓取 RSS/Atom 并写入缓存)
|
||||||
- `npm run build` 默认不联网;无缓存时 `articles` 页面会回退到 Phase 1 的站点入口展示
|
- `npm run build` 默认不联网;无缓存时 `articles` 页面会回退到 Phase 1 的站点入口展示
|
||||||
- articles 页面会按 `articles.yml` 的分类进行聚合展示:某分类下配置的来源站点,其文章会显示在该分类下
|
- articles 页面会按 `articles.yml` 的分类进行聚合展示:某分类下配置的来源站点,其文章会显示在该分类下
|
||||||
@@ -156,7 +161,7 @@ MeNav 配置系统采用“完全替换”策略(不合并),按以下优
|
|||||||
- 默认配置已将 `rss.cacheDir` 设为 `dev`(仓库默认 gitignore),避免误提交缓存文件;可按需改为自定义目录
|
- 默认配置已将 `rss.cacheDir` 设为 `dev`(仓库默认 gitignore),避免误提交缓存文件;可按需改为自定义目录
|
||||||
- GitHub Pages 部署工作流会在构建前自动执行 `npm run sync-articles`,并支持定时触发(默认每天 UTC 02:00;可在 `.github/workflows/deploy.yml` 调整)
|
- GitHub Pages 部署工作流会在构建前自动执行 `npm run sync-articles`,并支持定时触发(默认每天 UTC 02:00;可在 `.github/workflows/deploy.yml` 调整)
|
||||||
|
|
||||||
8. **GitHub(projects 热力图,可选)**
|
9. **GitHub(projects 热力图,可选)**
|
||||||
- `github.username`:你的 GitHub 用户名(用于 projects 页面标题栏右侧贡献热力图)
|
- `github.username`:你的 GitHub 用户名(用于 projects 页面标题栏右侧贡献热力图)
|
||||||
- `github.heatmapColor`:热力图主题色(不带 `#`,例如 `339af0`)
|
- `github.heatmapColor`:热力图主题色(不带 `#`,例如 `339af0`)
|
||||||
- `github.cacheDir`:projects 仓库元信息缓存目录(默认 `dev`,仓库默认 gitignore)
|
- `github.cacheDir`:projects 仓库元信息缓存目录(默认 `dev`,仓库默认 gitignore)
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ security:
|
|||||||
# 示例:
|
# 示例:
|
||||||
# allowedSchemes: [http, https, mailto, tel, obsidian, vscode]
|
# allowedSchemes: [http, https, mailto, tel, obsidian, vscode]
|
||||||
|
|
||||||
|
# 主题设置:默认明暗模式(可选)
|
||||||
|
# - mode: dark | light | system
|
||||||
|
# - dark/light:首屏默认主题;用户点击按钮切换后会写入 localStorage 并覆盖该默认值
|
||||||
|
# - system:跟随系统 prefers-color-scheme;用户手动切换后同样会写入 localStorage 并停止跟随
|
||||||
|
theme:
|
||||||
|
mode: dark # 可选: dark | light | system(默认 dark)
|
||||||
|
|
||||||
# 字体设置:全站基础字体
|
# 字体设置:全站基础字体
|
||||||
# - source: css | google | system
|
# - source: css | google | system
|
||||||
# - css: 通过 cssUrl 引入第三方字体 CSS
|
# - css: 通过 cssUrl 引入第三方字体 CSS
|
||||||
|
|||||||
@@ -16,11 +16,72 @@ module.exports = function initUi(state, dom) {
|
|||||||
// 移除预加载类,允许 CSS 过渡效果
|
// 移除预加载类,允许 CSS 过渡效果
|
||||||
document.documentElement.classList.remove('preload');
|
document.documentElement.classList.remove('preload');
|
||||||
|
|
||||||
// 应用从 localStorage 读取的主题设置(预加载阶段已写入 class)
|
let systemThemeMql = null;
|
||||||
|
let systemThemeListener = null;
|
||||||
|
|
||||||
|
function setTheme(isLight) {
|
||||||
|
const nextIsLight = Boolean(isLight);
|
||||||
|
state.isLightTheme = nextIsLight;
|
||||||
|
document.body.classList.toggle('light-theme', nextIsLight);
|
||||||
|
|
||||||
|
if (nextIsLight) {
|
||||||
|
themeIcon.classList.remove('fa-moon');
|
||||||
|
themeIcon.classList.add('fa-sun');
|
||||||
|
} else {
|
||||||
|
themeIcon.classList.remove('fa-sun');
|
||||||
|
themeIcon.classList.add('fa-moon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSystemThemeFollow() {
|
||||||
|
if (systemThemeMql && systemThemeListener) {
|
||||||
|
if (typeof systemThemeMql.removeEventListener === 'function') {
|
||||||
|
systemThemeMql.removeEventListener('change', systemThemeListener);
|
||||||
|
} else if (typeof systemThemeMql.removeListener === 'function') {
|
||||||
|
systemThemeMql.removeListener(systemThemeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
systemThemeMql = null;
|
||||||
|
systemThemeListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSystemThemeFollow() {
|
||||||
|
stopSystemThemeFollow();
|
||||||
|
|
||||||
|
try {
|
||||||
|
systemThemeMql = window.matchMedia('(prefers-color-scheme: light)');
|
||||||
|
} catch (e) {
|
||||||
|
systemThemeMql = null;
|
||||||
|
}
|
||||||
|
if (!systemThemeMql) return;
|
||||||
|
|
||||||
|
systemThemeListener = (event) => {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||||
|
stopSystemThemeFollow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTheme(Boolean(event && event.matches));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof systemThemeMql.addEventListener === 'function') {
|
||||||
|
systemThemeMql.addEventListener('change', systemThemeListener);
|
||||||
|
} else if (typeof systemThemeMql.addListener === 'function') {
|
||||||
|
systemThemeMql.addListener(systemThemeListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThemeMode() {
|
||||||
|
const raw = document.documentElement.getAttribute('data-theme-mode');
|
||||||
|
return raw ? String(raw).trim().toLowerCase() : 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用预加载阶段确定的主题(localStorage / site.theme.mode)
|
||||||
if (document.documentElement.classList.contains('theme-preload')) {
|
if (document.documentElement.classList.contains('theme-preload')) {
|
||||||
document.documentElement.classList.remove('theme-preload');
|
document.documentElement.classList.remove('theme-preload');
|
||||||
document.body.classList.add('light-theme');
|
setTheme(true);
|
||||||
state.isLightTheme = true;
|
} else {
|
||||||
|
setTheme(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用从 localStorage 读取的侧边栏状态(预加载阶段已写入 class)
|
// 应用从 localStorage 读取的侧边栏状态(预加载阶段已写入 class)
|
||||||
@@ -71,37 +132,53 @@ module.exports = function initUi(state, dom) {
|
|||||||
|
|
||||||
// 主题切换功能
|
// 主题切换功能
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
state.isLightTheme = !state.isLightTheme;
|
setTheme(!state.isLightTheme);
|
||||||
document.body.classList.toggle('light-theme', state.isLightTheme);
|
|
||||||
|
|
||||||
// 更新图标
|
// 用户手动切换后:写入 localStorage,并停止 system 跟随
|
||||||
if (state.isLightTheme) {
|
|
||||||
themeIcon.classList.remove('fa-moon');
|
|
||||||
themeIcon.classList.add('fa-sun');
|
|
||||||
} else {
|
|
||||||
themeIcon.classList.remove('fa-sun');
|
|
||||||
themeIcon.classList.add('fa-moon');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存主题偏好到 localStorage
|
|
||||||
localStorage.setItem('theme', state.isLightTheme ? 'light' : 'dark');
|
localStorage.setItem('theme', state.isLightTheme ? 'light' : 'dark');
|
||||||
|
stopSystemThemeFollow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化主题 - 已在页面加载前处理,此处仅完成图标状态初始化等次要任务
|
// 初始化主题 - 已在页面加载前处理,此处仅完成图标状态初始化等次要任务
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
// 从 localStorage 获取主题偏好
|
// 从 localStorage 获取主题偏好
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
|
||||||
// 更新图标状态以匹配当前主题
|
|
||||||
if (savedTheme === 'light') {
|
if (savedTheme === 'light') {
|
||||||
state.isLightTheme = true;
|
stopSystemThemeFollow();
|
||||||
themeIcon.classList.remove('fa-moon');
|
setTheme(true);
|
||||||
themeIcon.classList.add('fa-sun');
|
return;
|
||||||
} else {
|
|
||||||
state.isLightTheme = false;
|
|
||||||
themeIcon.classList.remove('fa-sun');
|
|
||||||
themeIcon.classList.add('fa-moon');
|
|
||||||
}
|
}
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
stopSystemThemeFollow();
|
||||||
|
setTheme(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未写入 localStorage:按 site.theme.mode 决定默认值
|
||||||
|
const mode = getThemeMode();
|
||||||
|
|
||||||
|
if (mode === 'light') {
|
||||||
|
stopSystemThemeFollow();
|
||||||
|
setTheme(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'system') {
|
||||||
|
let shouldUseLight = false;
|
||||||
|
try {
|
||||||
|
shouldUseLight =
|
||||||
|
window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
|
} catch (e) {
|
||||||
|
shouldUseLight = false;
|
||||||
|
}
|
||||||
|
setTheme(shouldUseLight);
|
||||||
|
startSystemThemeFollow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认 dark
|
||||||
|
stopSystemThemeFollow();
|
||||||
|
setTheme(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端菜单切换
|
// 移动端菜单切换
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" data-theme-mode="{{#if site.theme.mode}}{{site.theme.mode}}{{else}}dark{{/if}}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -18,7 +18,26 @@
|
|||||||
(function() {
|
(function() {
|
||||||
// 读取并应用主题设置
|
// 读取并应用主题设置
|
||||||
var savedTheme = localStorage.getItem('theme');
|
var savedTheme = localStorage.getItem('theme');
|
||||||
|
var defaultThemeMode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
|
||||||
|
defaultThemeMode = String(defaultThemeMode).trim().toLowerCase();
|
||||||
|
var shouldUseLight = false;
|
||||||
|
|
||||||
if (savedTheme === 'light') {
|
if (savedTheme === 'light') {
|
||||||
|
shouldUseLight = true;
|
||||||
|
} else if (savedTheme === 'dark') {
|
||||||
|
shouldUseLight = false;
|
||||||
|
} else if (defaultThemeMode === 'light') {
|
||||||
|
shouldUseLight = true;
|
||||||
|
} else if (defaultThemeMode === 'system') {
|
||||||
|
try {
|
||||||
|
shouldUseLight =
|
||||||
|
window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
|
} catch (e) {
|
||||||
|
shouldUseLight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseLight) {
|
||||||
document.documentElement.classList.add('theme-preload');
|
document.documentElement.classList.add('theme-preload');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +94,7 @@
|
|||||||
{{{navigation}}}
|
{{{navigation}}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sidebar-submenu-panel" aria-label="页面分类" data-container="sidebar-submenu"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-social" data-container="social-links">
|
<div class="sidebar-social" data-container="social-links">
|
||||||
|
|||||||
Reference in New Issue
Block a user