主题切换
灵动桌面框架内置暗色 / 亮色 / 跟随系统三态主题切换,由 CSS 变量、Ant Design 主题算法和 data-theme 属性三层配合实现。
| 层级 | 文件 | 职责 |
|---|---|---|
| CSS 变量 | src/styles/variables.css | 设计令牌,通过 :root[data-theme] 切换 |
| Ant Design | src/theme/antdTheme.ts | darkTheme / lightTheme,与 CSS 变量色值同步 |
| 状态管理 | src/store/app.ts | theme: "light" | "dark" | "system",Zustand 管理 |
| 应用入口 | src/App.tsx | 解析生效主题、写入 data-theme、选择 antd 主题 |
主题配置
// src/theme/antdTheme.ts(节选)
import type { ThemeConfig } from "antd";
import { theme } from "antd";
/** 暗色主题(与 variables.css :root[data-theme="dark"] 同步) */
export const darkTheme: ThemeConfig = {
algorithm: theme.darkAlgorithm,
token: {
...sharedToken,
colorPrimary: "#5090f0",
colorBgContainer: "#232325",
colorBgLayout: "#1a1a1c",
colorText: "#dcdcde",
colorBorder: "#353538",
// ...
},
components: {
...sharedComponents,
Tooltip: { colorBgSpotlight: "#2a2a2d", colorTextLightSolid: "#dcdcde" },
},
};
/** 亮色主题(与 variables.css :root[data-theme="light"] 同步) */
export const lightTheme: ThemeConfig = {
algorithm: theme.defaultAlgorithm,
token: {
...sharedToken,
colorPrimary: "#2563eb",
colorBgContainer: "#ffffff",
colorBgLayout: "#f5f5f7",
colorText: "#1a1a2e",
colorBorder: "#d9d9de",
// ...
},
components: {
...sharedComponents,
Tooltip: { colorBgSpotlight: "#1a1a2e", colorTextLightSolid: "#ffffff" },
},
};
/** 根据 resolved theme 获取对应 Ant Design 主题配置 */
export function getAntdTheme(resolved: "light" | "dark"): ThemeConfig {
return resolved === "dark" ? darkTheme : lightTheme;
}
两套主题除主色、背景、文本、边框外,还分别配置了 colorSuccess / colorWarning / colorError / colorInfo 和 colorTextSecondary / colorTextTertiary / colorBgElevated / colorBorderSecondary。sharedToken(圆角、13px 字号、控件高度)和 sharedComponents 见 Ant Design 组件。
改主题色要改两处
antdTheme.ts 的 token 和 variables.css 的 CSS 变量是手工保持同步的,改色值时两个文件要一起改。
主题状态管理
// src/store/app.ts(节选)
export type ThemeMode = "light" | "dark" | "system";
/** 解析实际生效的主题(system → 读取系统偏好) */
export function resolveTheme(mode: ThemeMode): "light" | "dark" {
if (mode !== "system") return mode;
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
const TOGGLE_ORDER: ThemeMode[] = ["dark", "light", "system"];
export const useAppStore = create<AppStore>((set) => ({
theme: "system",
toggleTheme: () =>
set((s) => {
const idx = TOGGLE_ORDER.indexOf(s.theme);
const next = TOGGLE_ORDER[(idx + 1) % TOGGLE_ORDER.length];
return { theme: next };
}),
setTheme: (theme) => set({ theme }),
// ...
}));
默认值是 "system",toggleTheme 按 dark → light → system → dark 循环。主题选择目前不持久化,重启后回到跟随系统。
应用主题
// src/App.tsx(节选)
const appTheme = useAppStore((s) => s.theme);
const [resolved, setResolved] = useState<"light" | "dark">(
resolveTheme(appTheme)
);
useEffect(() => {
// 非 system 模式直接应用
if (appTheme !== "system") {
setResolved(appTheme);
return;
}
// system 模式:立即解析 + 监听系统变化
setResolved(resolveTheme("system"));
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) =>
setResolved(e.matches ? "dark" : "light");
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [appTheme]);
// 将 resolved theme 写入 DOM,驱动 CSS 变量切换
useEffect(() => {
document.documentElement.setAttribute("data-theme", resolved);
}, [resolved]);
return (
<StyleProvider layer>
<ConfigProvider locale={zhCN} theme={getAntdTheme(resolved)}>
{/* ... */}
</ConfigProvider>
</StyleProvider>
);
同一个 resolved 值同时驱动两件事:写到 <html data-theme> 切换 CSS 变量,传给 getAntdTheme() 切换 antd 主题。跟随系统模式下,系统切换深浅色时应用会实时跟着变。
切换按钮
// src/components/ui/ThemeToggle.tsx
import { Button, Tooltip } from "antd";
import { Sun, Moon, Monitor } from "lucide-react";
import { useAppStore } from "@/store";
const THEME_CONFIG = {
dark: { icon: <Moon size={16} />, label: "暗色主题" },
light: { icon: <Sun size={16} />, label: "亮色主题" },
system: { icon: <Monitor size={16} />, label: "跟随系统" },
} as const;
/** 三态主题切换按钮:dark → light → system → dark */
export function ThemeToggle() {
const { theme, toggleTheme } = useAppStore();
const config = THEME_CONFIG[theme];
return (
<Tooltip title={config.label}>
<Button type="text" icon={config.icon} onClick={toggleTheme} />
</Tooltip>
);
}
ThemeToggle 已放在 AppLayout 顶栏右侧。需要直接选定某个模式(例如设置页里的下拉框)时,调用 setTheme("light") 即可。
CSS 变量适配
/* src/styles/variables.css(节选) */
/* ---- 暗色主题(默认) ---- */
:root,
:root[data-theme="dark"] {
--bg-primary: #1a1a1c;
--text-primary: #dcdcde;
--border: #353538;
}
/* ---- 亮色主题 ---- */
:root[data-theme="light"] {
--bg-primary: #ffffff;
--text-primary: #1a1a2e;
--border: #d9d9de;
}
在 <html> 还没写入 data-theme 之前,页面按暗色令牌渲染。完整令牌列表见 TailwindCSS 样式。
组件里取色的规则:
- Ant Design 体系内(如给
Layout、Sider设背景)→ 用antdTheme.useToken()拿token.colorBgContainer等 - 自定义元素 → 用
var(--xxx)CSS 变量 - 布局 / 间距 → TailwindCSS 原子类
import { theme as antdTheme } from "antd";
const { token } = antdTheme.useToken();
<Content style={{ padding: 24, background: token.colorBgLayout }}>
<div style={{ borderBottom: `1px solid var(--border)`, color: "var(--text-primary)" }}>
自适应主题内容
</div>
</Content>
不要用 Tailwind 的 dark: 前缀
TailwindCSS 4 的 dark: 默认跟随系统 prefers-color-scheme,框架没有把它绑定到 data-theme。应用内手动选择的主题与系统不一致时,dark: 样式会出错。需要随主题变化的颜色一律写成 CSS 变量。