跳到正文
灵动桌面框架
Esc
本栏目菜单

主题切换

灵动桌面框架内置暗色 / 亮色 / 跟随系统三态主题切换,由 CSS 变量、Ant Design 主题算法和 data-theme 属性三层配合实现。

层级文件职责
CSS 变量src/styles/variables.css设计令牌,通过 :root[data-theme] 切换
Ant Designsrc/theme/antdTheme.tsdarkTheme / lightTheme,与 CSS 变量色值同步
状态管理src/store/app.tstheme: "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 变量。