You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5.7 KiB
5.7 KiB
主题系统
文档版本:v1.1
最后更新:2025-01-18
目录
1. 技术方案
使用 CSS 变量 + class 切换实现主题系统,支持深色、亮色和跟随系统。
2. 主题 Store
// src/stores/themeStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
type Theme = "dark" | "light" | "system";
type ResolvedTheme = "dark" | "light";
interface ThemeState {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
}
// 获取系统主题偏好
const getSystemTheme = (): ResolvedTheme => {
if (typeof window === "undefined") return "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
// 应用主题到 DOM
const applyTheme = (theme: ResolvedTheme) => {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
};
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: "dark",
resolvedTheme: "dark",
setTheme: (theme) => {
const resolvedTheme = theme === "system" ? getSystemTheme() : theme;
applyTheme(resolvedTheme);
set({ theme, resolvedTheme });
},
}),
{
name: "theme-store",
onRehydrateStorage: () => (state) => {
if (state) {
const resolvedTheme =
state.theme === "system" ? getSystemTheme() : state.theme;
applyTheme(resolvedTheme);
state.resolvedTheme = resolvedTheme;
}
},
},
),
);
// 监听系统主题变化
if (typeof window !== "undefined") {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
const { theme, setTheme } = useThemeStore.getState();
if (theme === "system") {
setTheme("system"); // 重新计算 resolvedTheme
}
});
}
3. Theme Provider
// src/app/providers/ThemeProvider.tsx
import { useEffect } from "react";
import { useThemeStore } from "@/stores/themeStore";
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: "dark" | "light" | "system";
}
export function ThemeProvider({
children,
defaultTheme = "dark",
}: ThemeProviderProps) {
const { theme, setTheme } = useThemeStore();
// 初始化主题
useEffect(() => {
// 防止首次加载时的闪烁
const savedTheme = localStorage.getItem("theme-store");
if (!savedTheme) {
setTheme(defaultTheme);
}
}, []);
// 监听系统主题变化
useEffect(() => {
if (theme !== "system") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => setTheme("system");
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme, setTheme]);
return <>{children}</>;
}
4. 防止主题闪烁
在 index.html 的 <head> 中添加内联脚本:
<!-- index.html -->
<head>
<!-- ... -->
<script>
(function () {
try {
const stored = localStorage.getItem("theme-store");
const parsed = stored ? JSON.parse(stored) : null;
const theme = parsed?.state?.theme || "dark";
let resolvedTheme = theme;
if (theme === "system") {
resolvedTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
}
document.documentElement.classList.add(resolvedTheme);
} catch (e) {
document.documentElement.classList.add("dark");
}
})();
</script>
</head>
5. 主题切换组件
// src/components/common/ThemeToggle.tsx
import { Moon, Sun, Monitor } from "lucide-react";
import { useThemeStore } from "@/stores/themeStore";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslation } from "react-i18next";
export function ThemeToggle() {
const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useThemeStore();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
{resolvedTheme === "dark" ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
)}
<span className="sr-only">{t("settings.theme.toggle")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
{t("settings.theme.light")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
{t("settings.theme.dark")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Monitor className="mr-2 h-4 w-4" />
{t("settings.theme.system")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
6. Tailwind 深色模式配置
确保 tailwind.config.js 中配置了 class 策略:
// tailwind.config.js
module.exports = {
darkMode: ["class"], // 使用 class 策略
// ...
};
相关文档
最后更新:2025-01-18 | 版本:v1.1