第十章:暗夜模式与主题系统
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第十章:暗夜模式与主题系统
1. 主题系统设计
用户操作切换 or 系统偏好
│
▼
主题状态 (Signal)
│
├──> 更新 HTML data-theme 属性
│ │
│ ▼
│ CSS 变量自动切换
│ │
│ ▼
│ 全局样式更新
│
└──> 保存到 localStorage
│
▼
下次访问自动恢复
2. CSS 变量定义
2.1 亮色与暗色变量
/* main.css */
:root {
/* 亮色主题 (Catppuccin Latte) */
--bg: #eff1f5;
--card: #e6e9ef;
--border: #ccd0da;
--text: #4c4f69;
--secondary: #5c5f77;
--tertiary: #9ca0b0;
--primary: #1e66f5;
--primary-light: #d6e4fb;
--hover: #ccd0da;
}
[data-theme="dark"] {
/* 暗色主题 (Catppuccin Mocha) */
--bg: #1e1e2e;
--card: #181825;
--border: #313244;
--text: #cdd6f4;
--secondary: #bac2de;
--tertiary: #6c7086;
--primary: #89b4fa;
--primary-light: #2a4a7a;
--hover: #313244;
}
2.2 在组件中使用
rsx! {
// 组件只引用变量,不关心具体色值
div {
class: "min-h-screen flex flex-col",
style: "background: var(--bg); color: var(--text);",
main {
div {
class: "rounded-lg border p-5",
style: "background: var(--card); border-color: var(--border);",
h3 { "标题" }
p { style: "color: var(--tertiary);", "次要文本" }
}
}
}
}
3. 主题切换实现
3.1 主题 Context
use dioxus::prelude::*;
#[component]
fn ThemeProvider(children: Element) -> Element {
// 从 localStorage 读取或使用系统偏好
let theme = use_signal(|| {
let saved = get_local_storage("blog-theme");
saved.unwrap_or_else(detect_system_theme)
});
// 暴露给子组件
use_context_provider(|| theme);
// 主题变化时同步到 HTML
use_effect(move || {
set_html_attr("data-theme", &theme());
});
rsx! { {children} }
}
fn detect_system_theme() -> String {
// 通过 JS 检测系统偏好
web_sys::window()
.and_then(|w| w.match_media("(prefers-color-scheme: dark)").ok())
.flatten()
.map(|mq| if mq.matches() { "dark".to_string() } else { "light".to_string() })
.unwrap_or_else(|| "light".to_string())
}
fn get_local_storage(key: &str) -> Option<String> {
web_sys::window()?
.local_storage().ok()??
.get_item(key).ok()?
}
fn set_html_attr(attr: &str, value: &str) {
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
let _ = doc.document_element()
.map(|el| el.set_attribute(attr, value));
}
}
3.2 切换按钮
#[component]
fn ThemeToggle() -> Element {
let mut theme = use_context::<Signal<String>>();
let toggle = move |_| {
let next = if theme() == "light" {
"dark".to_string()
} else {
"light".to_string()
};
theme.set(next);
// 持久化到 localStorage
set_local_storage("blog-theme", &next);
};
rsx! {
button {
class: "theme-toggle w-[34px] h-[34px] flex items-center justify-center
rounded-lg cursor-pointer text-lg transition-all hover:scale-110",
style: "background: transparent; border: none; color: var(--secondary);",
onclick: toggle,
span {
// 显示对应的图标
if theme() == "light" { "🌙" } else { "☀️" }
}
}
}
}
fn set_local_storage(key: &str, value: &str) {
if let Some(window) = web_sys::window() {
let _ = window.local_storage().ok()
.flatten()
.map(|s| s.set_item(key, value));
}
}
4. 在 App 中使用
#[component]
fn App() -> Element {
rsx! {
// 在根组件包裹 ThemeProvider
ThemeProvider {
div { class: "min-h-screen",
style: "background: var(--bg); color: var(--text);",
Router::<Route> {}
}
}
}
}
5. 防止闪白(FOUC)
在页面加载时,如果主题尚未应用,用户可能会看到一闪的白色背景(Flash of Unstyled Content)。
解决方案:在 index.html 的 <head> 中添加内联脚本:
// 在 <head> 中立即执行,阻塞渲染
(function() {
var theme = localStorage.getItem('blog-theme');
if (theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
<!DOCTYPE html>
<html>
<head>
<script>
(function() {
var t = localStorage.getItem('blog-theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme:dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<!-- 其他样式和脚本 -->
</head>
6. 主题过渡动画
/* 平滑的主题切换过渡 */
html {
transition: background-color 0.3s, color 0.3s;
}
*, *::before, *::after {
transition: background-color 0.3s, border-color 0.3s, color 0.3s;
}
/* 如果用户减少了动画效果,则不使用过渡 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition: none !important;
}
}
7. 扩展:多种主题
/* 不仅可以亮/暗,还可以扩展更多主题 */
[data-theme="sepia"] {
--bg: #fbf1c7;
--card: #f2e5bc;
--border: #d5c4a1;
--text: #3c3836;
--primary: #d3869b;
}
[data-theme="forest"] {
--bg: #1b2b1b;
--card: #243824;
--border: #3a5a3a;
--text: #d4e7d4;
--primary: #7ec87e;
}
8. 图片适配
/* 暗夜模式下调暗图片 */
[data-theme="dark"] img {
filter: brightness(0.8) contrast(1.1);
transition: filter 0.3s;
}
/* 或使用 picture 元素提供两套图 */
picture {
source {
media: "(prefers-color-scheme: dark)",
srcset: "{dark_image}",
}
img { src: "{light_image}" }
}
9. 主题适配检查清单
- [ ] 所有 CSS 使用
var(--xxx)而非硬编码色值 - [ ] HTML
data-theme属性正确设置 - [ ] 初始加载无闪白(内联脚本)
- [ ] 切换动画平滑
- [ ] 用户偏好持久化到 localStorage
- [ ] 图片暗夜模式下调暗
- [ ] 代码块背景色适配
- [ ] 尊重
prefers-reduced-motion - [ ] 第三方组件(如图表库)适配
10. 小结
- CSS 变量实现了亮/暗主题的声明式管理
use_context_provider将主题状态全局共享use_effect同步主题到 HTML 和 localStorage- 内联脚本防止首次加载闪白
- 过渡动画提供平滑的切换体验
- 变量体系可扩展至多主题(如 Sepia、Forest 等)
- 至此你已完成前十章基础学习,掌握了 Dioxus 开发的核心技能。接下来的章节将深入进阶专题。
dioxusthemedark-modecss-variablespersistent