发布于 ,更新于 

博客现已支持手动切换深浅色模式

从现在开始,博客已经支持手动切换深浅色模式了。仅需点击页面左下角的最后一个按钮(移动端可拉到最下方点「🔁 切换主题」),或是试试点击这几个链接:深色模式浅色模式跟随系统

本来这里打了一长串字想解释来龙去脉的,直到后来我发现 hexo-theme-stellar 已有了一个比较粗暴的实现。所以这次仅提供修改时的流水账,供其他人魔改参考。
源码可参见 fa8e5d64277bc6 .

样式注入

定义 theme.style.darkmode == 'auto-switch' 为开关。
整体上通过写入 data-theme 选择器来实现,因为当前的逻辑是一启动就向 html 注入 data 属性,索性把 auto 也复制了一份情况。

source/css/_defines/theme.styl
1
2
3
4
5
6
if hexo-config('style.darkmode') == 'auto-switch'
:root[data-theme="dark"]
set_darkmode()
:root[data-theme="auto"]
@media (prefers-color-scheme: dark)
set_darkmode()

评论区组件仅测试了 Waline,通过变量定义配色板非常方便。

source/css/_plugins/comments/waline.styl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if hexo-config('style.darkmode') == 'auto-switch'
:root[data-theme="dark"] .wl-count
padding: .375em;
font-weight: bold;
font-size: 1.25em;
color: #fff;

:root[data-theme="dark"] .cmt-body.waline
--waline-white: #000;
--waline-light-grey: #666;
--waline-dark-grey: #999;
/* 布局颜色 */
--waline-color: #fff;
--waline-bg-color: #2b2f33b2;
--waline-bg-color-light: #272727;
--waline-border-color: #333;
--waline-disable-bg-color: #444;
--waline-disable-color: #272727;
/* 特殊颜色 */
--waline-bq-color: #272727;
/* 其他颜色 */
--waline-info-bg-color: #272727;
--waline-info-color: #666;

修改逻辑

修改了配置中 footer 的部分,仅需配置三个状态下的图标,方便不想把图标放在最后一位的用户。

_config.yml
1
2
3
4
5
6
footer:
social:
darkmode: # 键值为 darkmode 时配置为黑暗模式切换按钮(darkmode 须设置为 auto-switch),且此处仅按如下格式配置三个模式的图标
auto: '<img no-lazy width="24" height="24" src="/images/sun-moon.svg"/>'
light: '<img no-lazy width="24" height="24" src="/images/sun-fill.svg"/>'
dark: '<img no-lazy width="24" height="24" src="/images/moon-fill.svg"/>'

预处理后的按钮直接绑定 switchTheme() 事件。

layout/components/sidebar/sidebar.jsx
1
2
3
4
5
6
7
<div className="darkmode-switch"
id="darkmode-switch-light"
title={__('message.theme_switched.light')}
data-on-click="switchTheme()"
>
{parse(item.light)}
</div>

为了防止页面加载闪屏,在加载 main.css 之前,就需要做好属性注入。

layout/components/head.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const ImportDarkMode = (props) => {
const {theme} = props
const DarkModePreset = `
const themeModeList = ['light', 'dark', 'auto'];
var ThemeChange = (theme) => {
if(theme == null){
theme = 'auto';
}
document.documentElement.setAttribute('data-theme', theme)
window.localStorage.setItem('Stellaris.theme', theme);
}
ThemeChange(window.localStorage.getItem('Stellaris.theme'));
`
if (theme.style.darkmode == 'auto-switch') {
return <script type="text/javascript" data-no-instant="true" dangerouslySetInnerHTML={{__html: DarkModePreset}}/>
} else {
return <></>;
}
}

// 注意最后放在 ImportCSS 之前
<ImportDarkMode {...props}/>
<ImportCSS {...props}/>

动态按钮

本来想着动态插拔 class 属性,晚上又突然想到可以使用 data-theme 来实现。
按照约定好的顺序,依次在当前状态下展示下一个按钮即可。

source/css/_components/darkmode.styl
1
2
3
4
5
6
7
8
9
.darkmode-switch
display: none
cursor: pointer
:root[data-theme="dark"] #darkmode-switch-auto
display: inline-block
:root[data-theme="auto"] #darkmode-switch-light
display: inline-block
:root[data-theme="light"] #darkmode-switch-dark
display: inline-block

在页尾再加个监听 prefers-color-scheme 的事件,以便在系统切换时自动切换。最后把前文绑定的函数实现一下即可。

layout/components/scripts.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const ImportDarkModeListener = (props) => {
const {theme, __} = props
const DarkModeListener = `
const applyTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
window.localStorage.setItem('Stellaris.theme', theme);
const messages = {
light: '${__('message.theme_switched.light')}',
dark: '${__('message.theme_switched.dark')}',
auto: '${__('message.theme_switched.auto')}',
}
hud?.toast?.(messages[theme]);
}
const switchTheme = () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
let nextTheme = themeModeList[(themeModeList.indexOf(currentTheme) + 1) % themeModeList.length];
applyTheme(nextTheme);
}
var OSTheme = window.matchMedia('(prefers-color-scheme: dark)');
OSTheme.addEventListener('change', e => {
if (document.documentElement.getAttribute('data-theme') === 'auto') {
ThemeChange('auto');
}
})
`
if (theme.style.darkmode == 'auto-switch') {
return <script type="text/javascript" data-no-instant="true" dangerouslySetInnerHTML={{__html: DarkModeListener}}/>
} else {
return <></>;
}
}