切换基于antd的主题

一、说明antd4和antd5主题区别:

核心差异是 AntD4 是「静态 Less 变量」,AntD5 是「动态 CSS 变量」,这直接导致了主题切换的实现方式天差地别。

类型 作用时机 能否动态修改 典型场景
Less 变量 编译时(打包阶段) ❌ 不能(编译后变成固定 CSS 值) AntD4 的主题定制
CSS 变量 运行时(浏览器中) ✅ 可以(直接修改变量值,样式实时变化) AntD5 的主题切换

1、AntD4

AntD4 的样式是 基于 Less 静态编译的 ------ 所有主题变量(比如 @primary-color)在打包时就被编译成了固定的 CSS 值,举个例子:

AntD4 源码中的 Less 代码:

css 复制代码
// AntD4 按钮样式(Less)
.ant-btn {
  background: @primary-color; // Less 变量
  color: #fff;
}

打包编译后,@primary-color 会被替换成固定值(比如 #006bff),最终输出的 CSS 是:

css 复制代码
/* 编译后的 AntD4 CSS */
.ant-btn {
  background: #006bff; /* 固定值,无法动态修改 */
  color: #fff;
}

所以 AntD4 要切换主题,必须重新编译 Less 文件 (把 @primary-color 换成新值,再生成新的 CSS),然后替换页面中的样式。

2、AntD5

AntD5 把所有 Less 变量改成了 CSS 变量 (比如 --ant-primary-color),打包后输出的 CSS 是「变量引用形式」,而不是固定值:

AntD5 源码中的 Less 代码(最终编译成 CSS 变量):

css 复制代码
// AntD5 按钮样式(Less 编译为 CSS 变量)
.ant-btn {
  background: var(--ant-primary-color); // 引用 CSS 变量
  color: #fff;
}

打包后输出的 CSS 是:

css 复制代码
/* 编译后的 AntD5 CSS */
.ant-btn {
  background: var(--ant-primary-color); /* 保留变量引用,不替换成固定值 */
  color: #fff;
}

同时,AntD5 会在页面根节点(比如 <html>)注入默认的 CSS 变量:

css 复制代码
/* AntD5 自动注入的默认变量 */
:root {
  --ant-primary-color: #006bff;
  --ant-body-background: #fff;
  /* ...其他变量 */
}

所以 AntD5 切换主题,只需要修改根节点上的 CSS 变量值 (比如把 --ant-primary-color 改成 #f00),样式会实时变化 ------ 这就是为什么 AntD5 用 ConfigProvider 就能轻松切换主题,不需要编译。

维度 AntD4 AntD5
变量类型 Less 静态变量 CSS 动态变量
样式编译时机 打包时(固定值) 打包时(保留变量引用)
主题切换方式 动态编译 Less → 替换 CSS 文件 修改 CSS 变量值 → 实时生效
依赖 需要 less 库编译 无额外依赖(原生 CSS 支持)
切换性能 较慢(编译 + 替换样式) 极快(仅修改变量)

二、实现主题切换

1、antd4实现

1)通过less自己实现
css 复制代码
//1、style/theme/dark.less
@import 'antd/es/style/themes/default.less';
@import './variables.less';  //公共less主题变量
// 深色主题专属变量
@primary-color: #006bff; // 你的主题色
@body-background: #141414;
@component-background: #222222;
@text-color: rgba(255, 255, 255, 0.85);
@text-color-secondary: rgba(255, 255, 255, 0.65);


//2、style/theme/light.less
@import 'antd/es/style/themes/default.less';
@import './variables.less';
// 浅色主题专属变量
@primary-color: #006bff;
@body-background: #ffffff;
@component-background: #ffffff;
@text-color: rgba(0, 0, 0, 0.85);
@text-color-secondary: rgba(0, 0, 0, 0.65);
css 复制代码
//需要安装less:npm install less --save
//主题切换工具函数:theme-utils.ts
// @renderer/utils/theme-utils.ts
import less from 'less';

// 主题类型
export type ThemeType = 'dark' | 'light';

// 全局样式标签 ID(用于替换样式)
const THEME_STYLE_ID = 'antd-theme-style';

// 加载对应主题的 Less 文件(注意路径对应你项目的实际结构)
const loadThemeLess = async (theme: ThemeType): Promise<string> => {
  // 开发环境:直接加载本地 Less 文件
  if (import.meta.env.DEV) {
    const response = await fetch(`/src/style/theme/${theme}.less`);
    return response.text();
  }
  // 生产环境:需提前将主题 Less 打包为静态资源(或内联)
  // 示例:假设生产环境主题文件放在 dist/theme 下
  const response = await fetch(`/theme/${theme}.less`);
  return response.text();
};

// 编译并注入主题样式
export const switchTheme = async (theme: ThemeType) => {
  try {
    // 1. 加载主题 Less 代码
    const lessCode = await loadThemeLess(theme);

    // 2. 编译 Less(指定 AntD 前缀等配置)
    const { css } = await less.render(lessCode, {
      javascriptEnabled: true, // 允许 Less 中使用 JS
      modifyVars: {
        '@ant-prefix': 'ant', // 和你配置的前缀保持一致
      },
    });

    // 3. 替换页面中的主题样式
    let styleElement = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement;
    if (!styleElement) {
      styleElement = document.createElement('style');
      styleElement.id = THEME_STYLE_ID;
      document.head.appendChild(styleElement);
    }
    styleElement.innerHTML = css;

    // 4. 保存主题到本地存储(刷新后恢复)
    localStorage.setItem('antd-theme', theme);
  } catch (error) {
    console.error('切换主题失败:', error);
  }
};

// 初始化主题(页面加载时调用)
export const initTheme = () => {
  const savedTheme = localStorage.getItem('antd-theme') as ThemeType || 'light';
  switchTheme(savedTheme);
};

主题初始化:

javascript 复制代码
// @renderer/App.tsx
import { useEffect } from 'react';
import { initTheme } from './utils/theme-utils';

const App = () => {
  useEffect(() => {
    // 页面加载时初始化主题
    initTheme();
  }, []);

  return <div>你的应用内容</div>;
};

export default App;

切换主题:

css 复制代码
// @renderer/components/ThemeSwitch.tsx
import { Button } from 'antd';
import { switchTheme, ThemeType } from '../utils/theme-utils';

const ThemeSwitch = () => {
  const toggleTheme = async () => {
    const currentTheme = localStorage.getItem('antd-theme') as ThemeType || 'light';
    const newTheme = currentTheme === 'light' ? 'dark' : 'light';
    await switchTheme(newTheme);
  };

  return (
    <Button onClick={toggleTheme} type="primary">
      切换主题
    </Button>
  );
};

export default ThemeSwitch;

注意:开发环境可以直接通过 fetch 加载本地 Less 文件,但生产环境需要将主题 Less 文件打包为静态资源

vite配置:

css 复制代码
import { copyFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';

// 复制主题文件到 dist
const copyThemeFiles = () => {
  const srcDir = join(__dirname, 'src/style/theme');
  const distDir = join(__dirname, 'dist/renderer/theme');
  if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true });
  ['dark.less', 'light.less', 'variables.less'].forEach(file => {
    copyFileSync(join(srcDir, file), join(distDir, file));
  });
};

export default defineConfig({
  renderer: {
    // 其他配置...
    build: {
      // 打包完成后复制主题文件
      onEnd: () => copyThemeFiles(),
    },
  },
});
2)通过插件实现:如@zougt/vite-plugin-theme-preprocessor

主题变量配置不变,即style文件夹下的不变

css 复制代码
//需要安装:npm install @zougt/vite-plugin-theme-preprocessor -D

import { themePreprocessorPlugin } from '@zougt/vite-plugin-theme-preprocessor'

{
    plugins:[
      // ...其他配置
      themePreprocessorPlugin({
        less: {
          // 是否启用任意主题色模式,这里不启用
          arbitraryMode: false,
          // 在生产模式是否抽取独立的主题css文件,extract为true以下属性有效
          extract: true,
          // 默认主题
          defaultScopeName: 'theme-dark',
          // 提供多组变量文件
          multipleScopeVars: [
            {
              scopeName: 'theme-dark',
              path: resolve('src/renderer/src/style/theme/dark.less')
            },
            {
              scopeName: 'theme-light',
              path: resolve('src/renderer/src/style/theme/light.less')
            }
          ]
        }
      }),

      // ...其他配置
    ]
}

构建的产物:

css 复制代码
.theme-light .ant-btn {
  background-color: #1677ff;
}

.theme-dark .ant-btn {
  background-color: #177ddc;
}

运行时切换css:document.body.className = 'theme-dark'

话可以通过属性data-theme:

css 复制代码
//style/index.less
[data-theme='light'] {
  @import './theme/light.less';
}

[data-theme='dark'] {
  @import './theme/dark.less';
}

//生成的css为:
//[data-theme='light'] { ... }
//[data-theme='dark']  { ... }


//初始化
const theme = localStorage.getItem('theme') ?? 'light'
document.documentElement.setAttribute('data-theme', theme)

//切换
export function setTheme(theme: 'light' | 'dark') {
  document.documentElement.setAttribute('data-theme', theme)
  localStorage.setItem('theme', theme)
}

只是更改部分变量:使用less的modifyVars

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import less from 'less';

export default defineConfig({
  plugins: [react()],
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          '@primary-color': '#1890ff',
          '@link-color': '#1890ff',
        },
        javascriptEnabled: true,
      },
    },
  },
});

通过antd的cdn动态切换

javascript 复制代码
const changeTheme = (theme) => {
  const linkId = 'antd-theme-link';
  let link = document.getElementById(linkId);
  
  if (!link) {
    link = document.createElement('link');
    link.rel = 'stylesheet';
    link.id = linkId;
    document.head.appendChild(link);
  }
  
  if (theme === 'dark') {
    link.href = 'https://cdn.jsdelivr.net/npm/antd@4/dist/antd.dark.css';
  } else {
    link.href = 'https://cdn.jsdelivr.net/npm/antd@4/dist/antd.css';
  }
};

2、antd5

AntD v5 的主题由三层组成:Theme = Algorithm + Token + Component Token

Algorithm(主题算法)

  • defaultAlgorithm(亮色)

  • darkAlgorithm(暗色)

  • compactAlgorithm

决定:整体色板生成规则

global Token(全局设计变量):类似但 不等价于 v4 的 Less 变量:

css 复制代码
{
  colorPrimary,
  colorBgBase,
  colorTextBase,
  borderRadius,
}
复制代码
Component Token(组件级定制)
css 复制代码
{
  Button: {
    colorPrimary,
    borderRadius,
  }
}
复制代码
1、自定义主题变量
css 复制代码
// theme/index.ts
import { theme } from 'antd'

export type ThemeMode = 'light' | 'dark'

export const themeConfigMap = {
  light: {
    algorithm: theme.defaultAlgorithm,
    token: {
      colorPrimary: '#1677ff',
      colorBgBase: '#ffffff',
      colorTextBase: '#000000',
      borderRadius: 6,
    },
  },

  dark: {
    algorithm: theme.darkAlgorithm,
    token: {
      colorPrimary: '#177ddc',
      colorBgBase: '#141414',
      colorTextBase: '#ffffff',
      borderRadius: 6,
    },
  },
}
css 复制代码
import { ConfigProvider } from 'antd'
import { themeConfigMap, ThemeMode } from './theme'

interface Props {
  themeMode: ThemeMode
}

export function ThemeProvider({ themeMode, children }: React.PropsWithChildren<Props>) {
  return (
    <ConfigProvider theme={themeConfigMap[themeMode]}>
      {children}
    </ConfigProvider>
  )
}

使用:

css 复制代码
const App = () => {
  const [theme, setTheme] = useState<ThemeMode>('light')

  return (
    <ThemeProvider themeMode={theme}>
      <Button type="primary">Primary</Button>
      <Switch
        checked={theme === 'dark'}
        onChange={(v) => setTheme(v ? 'dark' : 'light')}
      />
    </ThemeProvider>
  )
}

js中读取token:

javascript 复制代码
import { theme } from 'antd'

const { useToken } = theme

const MyComponent = () => {
  const { token } = useToken()

  return <div style={{ color: token.colorPrimary }} />
}

antd5就不适合在独立搞一套和主题有关系的自定义变量,最好自定义antd的主题样式,但是一定要再单独搞一套业务的样式,可以如下:

javascript 复制代码
// 自定义主题变量
export const customThemeMap: Record<ThemeMode, CustomThemeTokens> = {
  light: {
    chartBg: '#ffffff',
    chartGrid: '#e5e5e5',
    orderBuy: '#2ecc71',
    orderSell: '#e74c3c',
    pnlPositive: '#16a085',
    pnlNegative: '#c0392b',
  },
  dark: {
    chartBg: '#0f0f0f',
    chartGrid: '#2a2a2a',
    orderBuy: '#2ecc71',
    orderSell: '#e74c3c',
    pnlPositive: '#1abc9c',
    pnlNegative: '#e74c3c',
  },
}


//使用context向下提供,之后通过style使用
import React from 'react'

export const CustomThemeContext =
  React.createContext<CustomThemeTokens | null>(null)

export const CustomThemeProvider: React.FC<{
  themeMode: ThemeMode
}> = ({ themeMode, children }) => {
  return (
    <CustomThemeContext.Provider value={customThemeMap[themeMode]}>
      {children}
    </CustomThemeContext.Provider>
  )
}

//通过style使用
const chartTheme = useContext(CustomThemeContext)
<div style={{ background: chartTheme.chartBg }} />

//和antd总体使用
<ConfigProvider theme={antdThemeMap[themeMode]}>
  <CustomThemeProvider themeMode={themeMode}>
    <App />
  </CustomThemeProvider>
</ConfigProvider>

若是自定义的变量有点多:

css 复制代码
/* @renderer/style/global.css */
/* 1. 基础变量(公共 fallback 值) */
:root {
  /* 主题 1:浅色主题(默认) */
  --primary-color: #006bff;
  --body-bg: #ffffff;
  --text-color: rgba(0, 0, 0, 0.85);
  --border-color: #d9d9d9;
  /* 可扩展更多变量:按钮、卡片、表单等 */
  --btn-primary-bg: var(--primary-color);
  --card-bg: #ffffff;
}

/* 2. 主题 2:深色主题(通过 .dark 类触发) */
:root.dark {
  --primary-color: #006bff; /* 可自定义深色主题的主色 */
  --body-bg: #141414;
  --text-color: rgba(255, 255, 255, 0.85);
  --border-color: #333333;
  --btn-primary-bg: var(--primary-color);
  --card-bg: #222222;
}

/* 3. 全局样式使用 CSS 变量(替代固定值) */
body {
  background-color: var(--body-bg);
  color: var(--text-color);
  margin: 0;
  padding: 0;
  transition: background-color 0.3s ease; /* 主题切换过渡动画 */
}

/* 示例:按钮样式使用变量 */
.btn-primary {
  background-color: var(--btn-primary-bg);
  color: #fff;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  padding: 8px 16px;
  cursor: pointer;
}

切换主题:

javascript 复制代码
// @renderer/utils/theme-utils.ts
export type ThemeType = 'light' | 'dark';

// 初始化主题(页面加载时执行)
export const initTheme = () => {
  // 优先读取本地存储的主题,默认浅色
  const savedTheme = localStorage.getItem('app-theme') as ThemeType || 'light';
  setTheme(savedTheme);
};

// 设置主题(核心函数)
export const setTheme = (theme: ThemeType) => {
  const root = document.documentElement; // 获取 :root 对应的 html 元素
  
  // 移除所有主题类名,避免冲突
  root.classList.remove('light', 'dark');
  // 添加当前主题类名
  root.classList.add(theme);
  
  // 持久化到本地存储
  localStorage.setItem('app-theme', theme);
};

// 切换主题(快捷函数)
export const toggleTheme = () => {
  const currentTheme = localStorage.getItem('app-theme') as ThemeType || 'light';
  const newTheme = currentTheme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
};

或者通过js动态设置:

javascript 复制代码
useEffect(() => {
  applyCustomTheme(customThemeMap[themeMode])
}, [themeMode])


function applyCustomTheme(theme: CustomThemeTokens) {
  const root = document.documentElement
  Object.entries(theme).forEach(([key, value]) => {
    root.style.setProperty(`--custom-${key}`, value)
  })
}


// 实际样式
.chart {
  background: var(--custom-chartBg);
}