第六章. 主题驱动:Provider、适配器与状态管理

第六章. 主题驱动:Provider、适配器与状态管理

在第五章,我们利用 Vanilla Extract 将 Design Tokens 编译成了强大的 CSS 变量系统。但这只是静态的“蓝图”。要让主题真正“活”起来——响应用户的操作(切换亮/暗模式、改变主题色),我们需要一个可靠的 状态管理 机制来存储和驱动这些变化。

本章,我们将引入轻量级的状态管理器 Zustand,创建 settingStore 来中心化管理所有与主题(及未来其他全局设置)相关的状态。随后,我们将构建核心的 ThemeProvider 组件,连接状态与 CSS 变量。最后,我们将编写 Ant Design 适配器 和配置 Tailwind CSS,让它们都能正确地“消费”我们精心设计的主题系统。

6.1. 状态管理:引入 Zustand

6.1.1. 决策:为何选择 Zustand?

架构师的思考
M

Redux Toolkit 功能强大、生态成熟,为什么不用它?

E
expert

Redux Toolkit 非常适合管理复杂、规范化、需要时间旅行调试的应用状态,例如大型电商的购物车、订单状态。

E
expert

但对于我们的主题设置这类相对简单的 UI 状态,引入 Redux 会带来大量的样板代码:需要定义 Slice, Reducers, Actions, Selectors,配置 Store 等,心智负担较重。

M

Zustand 的优势在哪里?

E
expert

Zustand 基于 Hooks API,极其简洁、轻量。

E
expert

它用一个简单的 create 函数就能创建 Store,几乎没有样板代码。

E
expert

并且它无需 Provider 包裹,可以在任何地方直接 import 并使用 Store Hook。

E
expert

对于管理主题、布局等全局 UI 配置状态,Zustand 是 2025 年更现代、更轻量、开发体验更好的选择。

E
expert

我们将把更复杂的 服务端状态 交给 React Query (后续章节引入)。

6.1.2. 安装 Zustand

我们使用 pnpm 将 Zustand 添加到项目依赖中。

1
pnpm add zustand

6.1.3. 扩展全局枚举

在编写 Store 之前,我们需要在我们 全局唯一的枚举文件 src/theme/types/enum.ts 中,追加 本章即将用到的 StorageEnum

我们在第五章已经定义了主题相关的枚举,现在我们只需在文件末尾添加新内容即可。

文件路径: src/theme/types/enum.ts (修改)

1
2
3
4
5
6
7
8
9
// --- 本章新增 ---

/**
* 定义用于 localStorage key 的枚举
* 集中管理 key 名称,避免在代码中硬编码字符串
*/
export enum StorageEnum {
Settings = "settings",
}

架构说明:保持所有相关的枚举(主题、存储键)都存放在 src/theme/types 目录下,有助于维护“主题系统”这一内聚模块的完整性和单一事实来源。

6.1.4. 渐进式构建 settingStore.ts

现在,我们在 src 目录下创建 store 目录和 settingStore.ts 文件。我们将从一个最简化的 Store 开始,逐步增强其功能。

第一步:创建 Store 目录与文件

1
2
3
# 在 src 目录下
mkdir store
touch store/settingStore.ts

第二步:创建最简化的 Store

我们的第一个目标,仅仅是创建一个能存储和更新主题模式 (themeMode) 的 Store。

文件路径: src/store/settingStore.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { create } from "zustand";
// 从我们唯一的枚举源导入
import { ThemeMode } from "@/theme/types/enum";

// 1. 定义我们需要管理的设置项的类型
export type SettingsType = {
themeMode: ThemeMode; // 主题模式 (light, dark)
};

// 2. 定义 Store 的完整类型,包含 state 和 action
type SettingStore = {
settings: SettingsType;
setThemeMode: (themeMode: ThemeMode) => void;
};
// 3. 使用 `create` 函数创建 Store
const useSettingStore = create<SettingStore>((set) => ({
// 4. 定义初始状态
settings: {
themeMode: ThemeMode.Light,
},
// 5. 定义更新状态的 action
// `set` 函数用于更新状态,它接受两种形式的参数:
// 形式 1: set(newState) - 直接传入新状态对象,会与当前状态进行浅合并
// 形式 2: set((state) => newState) - 传入一个函数,该函数接收当前完整状态作为参数,返回新状态对象
// 这里使用形式 2,因为我们需要基于当前状态来构建新状态
// state 参数: 当前 store 的完整状态对象 (包含 settings 和所有 actions)
// 返回值: 一个包含需要更新的状态片段的对象,Zustand 会自动将其与现有状态合并
setThemeMode: (themeMode) => {
set((state) => ({
settings: { ...state.settings, themeMode },
}));
},
}));

export default useSettingStore;

代码深度解析:

  • create<SettingStore>((set) => ({...})) 是 Zustand 的核心 API。它接收一个函数作为参数,这个函数的唯一参数 set 是一个用于更新状态的函数。
  • 我们返回的对象就是 Store 的初始内容,包含了初始状态 settings 和一个名为 setThemeMode 的 action。

第三步:重构 Store 以实现可扩展性

现在,我们扩展 SettingsType,并改进 Store 的结构,使其能管理所有与主题相关的设置,并让 actions 的组织更有条理。

文件路径: src/store/settingStore.ts (完整替换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { create } from "zustand";
import { FontFamilyPreset, typographyTokens } from "@/theme/tokens/typography";
// 从我们唯一的枚举源导入
import { ThemeColorPresets, ThemeMode } from "@/theme/types/enum";

// 1. 扩展 SettingsType,包含所有主题相关设置
export type SettingsType = {
themeColorPresets: ThemeColorPresets;
themeMode: ThemeMode;
fontFamily: string;
fontSize: number;
};

// 2. 改进 Store 类型,将所有 actions 组织在一个命名空间下
type SettingStore = {
settings: SettingsType;
actions: {
setSettings: (newSettings: Partial<SettingsType>) => void; // 使用 Partial 允许部分更新
};
};

const useSettingStore = create<SettingStore>((set) => ({
// 3. 提供更完整的初始状态
settings: {
themeColorPresets: ThemeColorPresets.Default,
themeMode: ThemeMode.Light,
fontFamily: FontFamilyPreset.openSans,
fontSize: Number(typographyTokens.fontSize.sm),
},
// 4. 将 action 的实现也放入 actions 命名空间
actions: {
setSettings: (newSettings) =>
set((state) => ({
settings: { ...state.settings, ...newSettings },
})),
},
}));


// 5. 导出更精细化的 Hooks (这是 Zustand 的推荐实践)
export const useSettings = () => useSettingStore((state) => state.settings);
export const useSettingActions = () => useSettingStore((state) => state.actions);

架构演进解析:

  • 我们将所有 actions 归于一个独立的命名空间,使得 Store 的结构更清晰:statesettingsmutationsactions
  • 我们提供了一个通用的 setSettings action,它接受一个 部分 (Partial) 的 SettingsType 对象,这让我们可以一次更新一个或多个设置项,非常灵活。
  • 我们导出了 useSettingsuseSettingActions 两个 Hooks。这种做法可以优化 React 的重渲染。如果一个组件只需要调用 action 而不关心状态的变化,它只使用 useSettingActions 就不会因为 settings 的改变而触发不必要的重渲染。

第四步:引入持久化 (persist middleware)

用户选择的主题(亮/暗、颜色)应该在刷新页面后 保持不变。我们需要将这些设置持久化到浏览器的 localStorage 中。

文件路径: src/store/settingStore.ts (在 import { create }... 后追加)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { create } from 'zustand';
// 1. 导入 zustand/middleware
import { createJSONStorage, persist } from 'zustand/middleware';
import { FontFamilyPreset, typographyTokens } from '@/theme/tokens/typography';
// 导入 StorageEnum 以及其他枚举
import { StorageEnum, ThemeColorPresets, ThemeMode } from '@/theme/types/enum';

// ... (SettingsType 和 SettingStore 类型定义保持不变) ...

// 2. 使用 persist 中间件包裹 create
const useSettingStore = create<SettingStore>()(
  persist(
    (set) => ({
      // 3. 内部的 Store 定义 (settings 和 actions) 保持不变
      settings: {
        themeColorPresets: ThemeColorPresets.Default,
        themeMode: ThemeMode.Light,
        fontFamily: FontFamilyPreset.openSans,
        fontSize: Number(typographyTokens.fontSize.sm),
      },
      actions: {
        setSettings: (newSettings) =>
          set((state) => ({ settings: { ...state.settings, ...newSettings } })),
      },
    }),
    // 4. persist 中间件的配置对象
    {
      // 使用 StorageEnum.Settings 作为 key
      name: StorageEnum.Settings,
      
      // 指定存储介质
      storage: createJSONStorage(() => localStorage),
      
      // 关键优化:只持久化 'settings' 部分,忽略 'actions'
      partialize: (state) => ({ settings: state.settings }),
    }
  )
);

// ... (useSettings 和 useSettingActions 的导出保持不变)

persist 中间件深度解析:

  • 工作模式persist 是一个 高阶函数(函数返回函数),它像“洋葱皮”一样包裹住我们原来的 Store 定义。当状态发生变化时,persist 会拦截这次变化,并将新的状态(根据配置)自动保存到指定的存储中。
  • name: 指定存储在 localStorage 中的键名。我们使用 StorageEnum.Settings 来避免硬编码字符串,这是企业级开发的良好实践。
  • storage: 显式指定使用 localStorage 作为存储介质。createJSONStorage 会自动处理 JavaScript 对象和 JSON 字符串之间的转换。
  • partialize 的价值: 这是 性能和安全性的关键优化。默认情况下,persist 会尝试存储 Store 中的 所有 数据。但我们的 actions 对象中包含的是函数,函数无法被 JSON 序列化,会导致报错或无效存储。partialize 允许我们精确地告诉 persist:“嘿,你只需要关心 state.settings 就行了,忽略 actions。”这确保了只有纯粹的状态数据被持久化。

阶段性成果:我们成功引入了 Zustand,并创建了 settingStore 来中心化管理应用的主题设置。通过 极其循序渐进 的构建,我们从一个最小化的 Store 开始,逐步扩展并最终集成了 持久化 功能。我们深度解析了 persist 中间件的每一个配置项,特别是 partialize 的优化价值。导出的 useSettingsuseSettingActions Hooks 为后续 ThemeProvider 消费这些状态做好了准备。


6.2. 构建 ThemeProvider

6.1 节,我们使用 Zustand 创建了 settingStore,它现在是我们应用主题状态的“唯一事实来源”。在第五章,我们利用 Vanilla Extract 将 Design Tokens 编译成了对应不同主题模式和颜色预设的 CSS 变量集,它们由 <html> 根元素上的 data-* 属性激活。

现在,我们需要一个“总控制器”来完成这最后一步:读取 settingStore 中的状态,并将这些状态实时地同步到 <html> 元素的属性上。这个总控制器,就是我们的 ThemeProvider React 组件。

6.2.1. 核心职责:状态订阅与 DOM 操作

ThemeProvider 的核心职责非常明确和聚焦:

  1. 订阅状态:使用 useSettings Hook 实时监听 settingStore 中与主题相关的状态变化。
  2. 执行副作用:当状态发生变化时,通过 React 的 useEffect Hook 执行副作用操作——直接修改 document.documentElement (<html> 标签) 的 data-* 属性或 style 属性,从而激活我们在 CSS 中定义的变量。

6.2.2. 渐进式构建 ThemeProvider.tsx

我们将在 src/theme/ 目录下创建 theme-provider.tsx 文件,并一步步实现其功能。

第一步:创建组件骨架并订阅状态

我们先创建 ThemeProvider 组件的基本结构,并使用 useSettings Hook 来获取当前的主题设置。

文件路径: src/theme/theme-provider.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import type React from "react";
import { useEffect } from "react";
import { useSettings } from "@/store/settingStore";
import { HtmlDataAttribute } from "@/theme/types/enum";

// 1. 定义 Props 类型,它目前只需要接收 children
interface ThemeProviderProps {
children: React.ReactNode;
}

export function ThemeProvider({ children }: ThemeProviderProps) {
// 2. 使用 useSettings Hook 获取当前的 settings 对象
// 这一步就完成了对 Zustand store 状态的“订阅”
const settings = useSettings();

// --- 我们将在这里添加 useEffects ---

// 3. 直接渲染子组件
return <>{children}</>;
}

代码解析:

  • 通过调用 useSettings()ThemeProvider 组件就 订阅settingStore 的状态。当 settings 对象在 Zustand 中发生变化时,这个组件会自动重新渲染,从而获取到最新的 settings 值。

第二步:同步 themeMode,激活亮/暗模式

现在,我们添加第一个 useEffect,用于将 themeMode 状态同步到 <html> 元素的 data-theme-mode 属性上。

文件路径: src/theme/theme-provider.tsx (在 settings 下方添加)

1
2
3
4
5
6
7
8
9
10
const settings = useSettings();

// 第一个 Effect:同步 themeMode
useEffect(() => {
const root = window.document.documentElement; // 获取 <html> 元素

// 将 store 中的 themeMode ('light' or 'dark') 设置为 data-theme-mode 属性
root.setAttribute(HtmlDataAttribute.ThemeMode, settings.themeMode);

}, [settings.themeMode]); // 依赖项数组:只在 themeMode 变化时才执行此 effect

useEffect 深度解析:

  • 副作用处理: 在 React 函数组件中,直接修改 DOM(如 setAttribute)属于 副作用 (Side Effect)useEffect 是 React 提供的、用于处理这类副作用的标准 Hook。它确保了 DOM 操作在 React 完成渲染 之后 执行。
  • 依赖项数组 [settings.themeMode]: 这是 useEffect 的性能关键。这个数组告诉 React:“请监听 settings.themeMode 这个值。只有当它发生变化时(例如从 'light' 变成 'dark'),才需要重新执行这个 effect 函数”。这避免了在其他设置(如字体大小)变化时,也去执行不必要的 setAttribute 操作。

连接第五章: 当这个 useEffect 执行,<html> 标签上就会被添加 data-theme-mode="light"data-theme-mode="dark" 属性。这会 精确地激活 我们在 theme.css.ts 中使用 createGlobalTheme 为对应选择器 (:root[data-theme-mode="..."]) 生成的 那一整套 CSS 变量!亮/暗模式切换的核心机制就此打通。

第三步:同步 themeColorPresets,激活动态主题色

我们以完全相同的方式,添加第二个 useEffect 来同步主题色预设。

文件路径: src/theme/theme-provider.tsx (追加第二个 useEffect)

1
2
3
4
5
6
7
8
9
10
// ... 第一个 useEffect ...

// 第二个 Effect:同步 themeColorPresets
useEffect(() => {
const root = window.document.documentElement;

// 将 store 中的 themeColorPresets ('default', 'cyan'...) 设置为 data-color-palette 属性
root.setAttribute(HtmlDataAttribute.ColorPalette, settings.themeColorPresets);

}, [settings.themeColorPresets]); // 依赖项:只在 themeColorPresets 变化时执行

连接第五章: 当这个 useEffect 执行,<html> 标签上的 data-color-palette 属性就会被更新(例如 data-color-palette="cyan")。这会 精确地激活 我们在 theme.css.ts 中使用 globalStyleassignVars 为该选择器生成的、用于覆盖主品牌色的 CSS 变量 规则!动态主题色切换的核心机制也打通了。

第四步:同步字体与基础字号

对于字体和基础字号,我们直接修改 <html> (针对 fontSize) 和 <body> (针对 fontFamily) 的 style 属性。

文件路径: src/theme/theme-provider.tsx (追加第三个 useEffect)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ... 前两个 useEffects ...

// 第三个 Effect:同步字体和基础字号
useEffect(() => {
const root = window.document.documentElement;
const body = window.document.body;

// 将基础字号应用到 <html> 的 style.fontSize 上
// 这将作为整个应用 rem 单位的计算基准
root.style.fontSize = `${settings.fontSize}px`;

// 将字体族应用到 <body> 的 style.fontFamily 上
// 它将作为整个页面的默认字体被继承
body.style.fontFamily = settings.fontFamily;

}, [settings.fontFamily, settings.fontSize]); // 依赖项:字体或字号任一变化时执行

6.2.3. 应用 ThemeProvider 并验证

ThemeProvider 组件已经构建完成。现在,我们需要在应用的根部使用它,来包裹我们的整个应用。

文件路径: src/main.tsx (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import ReactDOM from 'react-dom/client';
import MyApp from '@/MyApp';
import '@/index.css';
import { ConfigProvider, App as AntdApp } from 'antd';
import '@/theme/theme.css';
import { ThemeProvider } from '@/theme/theme-provider'; // 1. 导入 ThemeProvider

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
{/* 2. 在所有组件的最外层包裹 ThemeProvider */}
<ThemeProvider>
<ConfigProvider> {/* 我们暂时保留 ConfigProvider,下一节会处理它 */}
<AntdApp>
<MyApp />
</AntdApp>
</ConfigProvider>
</ThemeProvider>
</React.StrictMode>,
);

验证

  1. 重启开发服务器 (pnpm dev)。
  2. 打开浏览器开发者工具,检查 <html> 元素。你会发现它已经被添加了 data-theme-mode="light"data-color-palette="default" 属性,并且 style 属性中设置了 font-size,这正是我们在 Zustand 的预设 State 的功劳

image-20251022201835632


6.3. 实现 Ant Design 适配器

在前面的章节,我们的 ThemeProvider 已经成功地将全局主题状态(如 'dark' 模式、'cyan' 色)转换为全局 CSS 变量(如 data-theme-mode="dark"),并应用到了 <html> 元素上。

这套机制对于使用原生 CSS 组件已经可以完美工作。但是,我们的核心组件库 Ant Design,拥有一个独立的主题系统,它默认 不消费 CSS 变量。

6.3.1. 问题的识别:主题系统的“语言”不通

Ant Design 的样式由一个名为 ConfigProvider 的 React Context 组件控制。它不读取 CSS 变量,而是接收一个 JavaScript theme 对象(例如 { token: { colorPrimary: '#...', fontSize: 16 } })来为其所有子组件注入样式。

这就导致了一个核心问题:ThemeProvider 广播的“CSS 变量信号”,Ant Design“听不懂”。这将导致我们的 Ant Design 组件(如 Button, Menu)与页面的其他部分(如布局背景、原生文本)在主题上完全脱节。

我们的目标是:让 Ant Design 能够完全理解并动态响应我们的全局主题状态。

6.3.2. 架构决策:规避“紧耦合”陷阱

最直观的错误想法,是在 ThemeProvider 内部“顺便”处理 Ant Design 的主题。

反面教材(错误的设计):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这是一个错误的设计,请勿模仿
import { ConfigProvider, theme as antdTheme } from 'antd'; // 致命污染
import { useSettings } from "@/store/settingStore";
// ...

export function ThemeProvider({ children }) {
const settings = useSettings();

// 1. 原有的 DOM 操作
useEffect(() => { /* ... */ }, [settings.themeMode]);

// 2. 新增的 AntD "翻译" 逻辑
const algorithm = settings.themeMode === 'light' ? ... : ...;
const token = { colorPrimary: ..., fontSize: settings.fontSize };

// 3. 混合返回
return (
<ConfigProvider theme={{ algorithm, token }}>
{children}
</ConfigProvider>
);
}

这种设计会导致两个严重问题:

  1. 紧耦合ThemeProvider 的核心职责是管理通用主题状态。一旦它 import { ConfigProvider },它就与 antd 这个 具体 的 UI 库“锁死”了。
  2. 违反开闭原则:此原则要求软件“对扩展开放,对修改关闭”。
    • 无法扩展:如果我们想再支持 Material UI (MUI),我们就必须 修改 ThemeProvider 的内部,加入 MuiThemeProvider 和另一套“翻译”逻辑,使其代码加倍臃肿。
    • 难以维护:如果我们想 替换 AntD,我们就必须深入 ThemeProvider 的核心代码进行“外科手术”。
架构师的思考
M

我明白了。直接在 ThemeProvider 里处理,会让它变得臃肿且脆弱。那正确的架构思路是什么?

E
expert

我们需要一个“翻译官”。这个翻译官的角色,专门负责将我们“通用的主题语言”翻译成“Ant Design 的特定语言”。

M

听起来像是一种设计模式?

E
expert

正是 适配器模式。我们将为每个需要接入我们主题系统的第三方 UI 库,创建一个专属的适配器。比如 AntdAdapterMuiAdapter 等。

M

这样做的好处是?

E
expert

彻底解耦ThemeProvider 保持纯净,只管广播通用主题。AntdAdapter 则订阅这个主题,并负责与 Ant Design 的 ConfigProvider 进行所有“对话”。添加或移除对某个 UI 库的支持,只需要增加或删除一个适配器文件,ThemeProvider 核心代码完全不用动。

6.3.3. 第一步:构建“翻译官” AntdAdapter

AntdAdapter 是一个独立的 React 组件,它的职责非常单一:订阅全局 settings,将其“翻译”为 AntD theme 对象,并渲染 ConfigProvider

1. 准备“翻译工具” (removePx)

我们的主题令牌(baseThemeTokens)中,borderRadius 等值是 "8px" 这样的字符串。Ant Design 的 token 对象需要 8 这样的数字。我们首先创建一个工具函数来处理这种转换。

文件路径: src/theme/utils/themeUtils.ts (添加)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 从字符串值中移除 'px' 单位并转换为数字。
* @param value 示例: "16px", "16.5px", "-16px", "16", 16
* @returns 示例: 16, 16.5, -16, 16, 16
* @throws 如果值无效,则抛出错误
*/
export const removePx = (value: string | number): number => {
if (typeof value === "number") return value;
if (!value) throw new Error("Invalid value: empty string");
const trimmed = value.trim();
// 使用正则表达式测试 'px' 是否在末尾(不区分大小写)
const hasPx = /px$/i.test(trimmed);
// 如果有 'px',则截取掉最后两个字符;否则直接使用
const num = hasPx ? trimmed.slice(0, -2) : trimmed;
// 转换为浮点数
const result = Number.parseFloat(num);
// 校验转换结果是否是一个有效数字
if (Number.isNaN(result)) {
throw new Error(`Invalid value: ${value}`);
}
return result;
};

2. 编写 AntdAdapter 核心逻辑

现在,我们创建 AntdAdapter 组件。它符合我们最早的时候创建的 UILibraryAdapter 接口(({ mode, children }) => React.ReactElement),在这里,我们可以把坑填上了,我们将他定义为枚举

文件路径: src/theme/type.ts (修改)

1
2
3
4
5
6
7
8
9
import type { ThemeMode } from "./types/enum";
/**
* UI 库适配器的 Props 类型定义
*/
export type UILibraryAdapterProps = {
mode: ThemeMode; // 定义为枚举
children: React.ReactNode;
};
export type UILibraryAdapter = React.FC<UILibraryAdapterProps>;

文件路径: src/theme/adapter/antd.adapter.tsx (新建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// Antd的必要依赖
import type { ThemeConfig } from "antd";
import { App as AntdApp, ConfigProvider, theme } from "antd";
// Zustand仓库
import { useSettings } from "@/store/settingStore";
// 枚举
import { ThemeMode } from "@/theme/types/enum";
// 工具函数
import { removePx } from "@/theme/utils/themeUtils";
// 基础主题令牌
import { baseThemeTokens } from "../tokens/base";
import {
darkColorTokens,
lightColorTokens,
presetsColors,
} from "../tokens/color";
import type { UILibraryAdapter } from "../type";

export const AntdAdapter: UILibraryAdapter = ({
mode,
children,
}): React.ReactNode => {
// 1. 订阅全局 store,获取动态设置
const { themeColorPresets, fontFamily, fontSize } = useSettings();
// 2. 翻译规则 1:将我们的 mode 映射为 antd 的 algorithm
const algorithm =
mode === ThemeMode.Light ? theme.defaultAlgorithm : theme.darkAlgorithm;

// 3.准备翻译原材料
const colorTokens =
mode === ThemeMode.Light ? lightColorTokens : darkColorTokens;
const primaryColorToken = presetsColors[themeColorPresets];

// 3. 核心翻译:构建 antd 的 theme.token 对象
const token: ThemeConfig["token"] = {
// a. 颜色映射
colorPrimary: primaryColorToken.default,
colorSuccess: colorTokens.palette.success.default,
colorWarning: colorTokens.palette.warning.default,
colorError: colorTokens.palette.error.default,
colorInfo: colorTokens.palette.info.default,
colorBgLayout: colorTokens.background.default,
colorBgContainer: colorTokens.background.paper,
colorBgElevated: colorTokens.background.default,

// b. 字体映射
fontFamily: fontFamily,
fontSize: fontSize,

// c. 圆角映射 (使用 removePx 工具函数进行转换)
borderRadius: removePx(baseThemeTokens.borderRadius.default),
borderRadiusLG: removePx(baseThemeTokens.borderRadius.lg),
borderRadiusSM: removePx(baseThemeTokens.borderRadius.sm),

// d. 其他 antd 配置 : wireframe是Antd5的新特性,用于禁用组件的阴影效果以便覆盖我们的阴影效果
wireframe: false,
};

// 4. (可选) 对特定组件的精细化翻译
const components: ThemeConfig["components"] = {
Breadcrumb: {
separatorMargin: removePx(baseThemeTokens.spacing[1]),
},
Menu: {
colorFillAlter: "transparent",
itemColor: colorTokens.text.secondary,
},
};

// 5. 组装并返回最终的 Provider
return (
<ConfigProvider theme={{ algorithm, token, components }}>
{/* 必须再次包裹 <App>,这是 AntD v5+ 的要求。
它能确保 message.success()、Modal.confirm()
等静态方法能正确继承 ConfigProvider 的主题。
*/}
<AntdApp>{children}</AntdApp>
</ConfigProvider>
);
};

这个适配器现在是完全独立的。它订阅 useSettings,接收 mode,并输出一个配置好的 ConfigProvider

6.3.4. 第二步:升级 ThemeProvider 以支持适配器

现在,我们必须升级 ThemeProvider,使其拥有一个“插槽”来承载 AntdAdapter(以及未来可能的其他适配器),而 不需要知道 AntdAdapter 的具体实现。

文件路径: src/theme/theme-provider.tsx (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import type React from "react";
import { useEffect } from "react";
import { useSettings } from "@/store/settingStore";
import type { UILibraryAdapter } from "./type"; // ✨ 1. 导入 UILibraryAdapter 类型
import { HtmlDataAttribute } from "./types/enum";

// ✨ 2. 升级 Props 类型,增加一个可选的 adapters 数组
interface ThemeProviderProps {
children: React.ReactNode;
adapters?: UILibraryAdapter[]; // adapters 是一个符合特定接口的组件数组
}

export function ThemeProvider({ children, adapters = [] }: ThemeProviderProps) {
const settings = useSettings();

// --- 所有的 useEffect 逻辑 (广播 CSS 变量) 保持完全不变 ---

// ✨ 3. 新增核心逻辑:使用 reduceRight 动态包裹子组件
const wrappedWithAdapters = adapters.reduceRight(
(children, Adapter) => (
<Adapter key={Adapter.name} mode={settings.themeMode}>
{children}
</Adapter>
),
children, // 初始值是原始的 children
);

// ✨ 4. 返回被适配器包裹后的子组件树
return <>{wrappedWithAdapters}</>;
}

通过这次升级,ThemeProvider 具备了“适配器插槽”的能力。它只依赖 UILibraryAdapter 这个 类型,从而与所有 具体 的适配器实现(如 AntdAdapter)保持了解耦。

6.3.5. 第三步:集成并验证

最后一步,我们在应用入口 main.tsx 中,将 AntdAdapter“插入”到 ThemeProvider 的插槽中。

文件路径: src/main.tsx (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
import ReactDOM from "react-dom/client";
import MyApp from "@/MyApp";
import "@/theme/theme.css"; // <-- 导入我们的 VE 样式文件
// 1. 导入我们创建的 AntdAdapter
import { AntdAdapter } from "@/theme/adapter/antd.adapter";
import { ThemeProvider } from "@/theme/theme-provider";

import "@/index.css";
const rootElement = document.getElementById("root");
if (rootElement) {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<ThemeProvider adapters={[AntdAdapter]}>
<MyApp />
</ThemeProvider>
</React.StrictMode>,
);
}

最终工作流程

  1. 应用启动,ThemeProvider 渲染,并传入 adapters={[AntdAdapter]}
  2. ThemeProvider 内部,useEffect 开始工作,将 settings 状态同步为 <html> 上的 CSS 变量。
  3. 同时,ThemeProviderreduceRight 逻辑执行,生成 <AntdAdapter mode={...}><MyApp /></AntdAdapter> 的 TSX 结构。
  4. React 开始渲染 AntdAdapterAntdAdapter 内部订阅 useSettings,并将 settings “翻译”为 AntD theme 对象。
  5. AntdAdapter 渲染出 <ConfigProvider theme={...}><App><MyApp /></App></ConfigProvider>
  6. 最终,MyApp 及其内部所有组件,都被正确地包裹在两个主题系统(CSS 变量 和 AntD ConfigProvider)中,两者均由 useSettings 同一个数据源驱动,保持了完美同步。

阶段性成果:我们成功地完成了主题系统的闭环。通过 创建 AntdAdapter升级 ThemeProvider最终集成 这三个清晰的步骤,我们构建了一个高内聚、低耦合、可扩展的主题架构。现在,Ant Design 组件已经能够完全响应我们全局主题状态的任何变化。


6.4. 配置 Tailwind 消费 Tokens:实现样式统一

我们已经让 Ant Design 能够响应我们的主题系统。现在,轮到混合样式架构的另一位主角——Tailwind CSS 了。

目标:确保我们使用 Tailwind 原子类(如 bg-primary, p-4, rounded-md, lg:text-xl)构建的自定义布局和组件,其视觉效果(颜色、间距、字体、断点等)与我们的 Design Tokens 完全一致,并能 动态响应 主题切换。

核心思路:我们将配置 Tailwind,使其 theme 对象中的值,不再 使用 Tailwind 的默认值或硬编码值,而是 引用 我们在第五章通过 Vanilla Extract 生成的 全局 CSS 变量

6.4.1. 第一步:在 CSS 中配置 darkMode 策略

Tailwind v4 推荐将 darkMode 策略定义在 CSS 文件中。我们需要告诉 Tailwind 如何识别暗黑模式。在 Prorise-Admin 中,我们通过 ThemeProvider<html> 元素上添加 data-theme-mode="dark" 属性来标识暗黑模式。

文件路径: src/index.css (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* import tailwind */
/* 我们仍然使用完整导入,因为我们需要依赖 Preflight 的某些特性 */
/* 并且通过 @layer base 进行了必要的覆盖 */
@import "tailwindcss";
@import "tw-animate-css"; /* (可选) 引入动画库 */

/* config tailwind */
/* 告诉 Tailwind 去加载 JS 配置文件 (下一步创建) */
@config "../tailwind.config.ts";

/* base layer */
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
/* 覆盖 Preflight 的默认 border-color,使用我们 VE 生成的灰色变量 */
border-color: rgba(var(--colors-palette-gray-500Channel) / var(--opacity-border));
}
body {
/* (可选) 设置基础背景色,引用我们 VE 生成的变量 */
background-color: var(--colors-background-default);
}
/* ... 其他必要的 base layer 覆盖 ... */
}

/* @theme: 用于定义自定义工具类、动画等,而不是核心主题值 */
@theme {
}

/* --- 新增:定义暗黑模式变体 --- */
/*
这行代码告诉 Tailwind v4:
当任何元素或其祖先元素匹配选择器 `:is([data-theme-mode="dark"] *)` 时
(即 html 标签上有 data-theme-mode="dark"),
就应用以 `dark:` 为前缀的工具类。
这完美匹配了我们 ThemeProvider 的行为。
*/
@variant dark (&:is([data-theme-mode="dark"] *));

关键配置解析

  • @config "../tailwind.config.ts";: 明确告诉 Tailwind v4 去加载我们的 JS 配置文件。这是必需的,因为 v4 不再自动查找。
  • @variant dark (&:is([data-theme-mode="dark"] *));: 这是 v4 中配置 darkMode: 'selector'新方式。它精确地将 Tailwind 的 dark: 变体与我们 ThemeProvider 设置的 data-theme-mode="dark" 属性关联起来。
  • @layer base 覆盖: 我们保留了对 border-color 的覆盖,确保边框颜色使用我们 VE 定义的灰色变量和透明度。

6.4.2. 第二步:创建/配置 tailwind.config.ts 映射 Tokens

现在,我们来配置 tailwind.config.ts。它的核心作用是将我们 src/theme/tokens/ 中的 结构 映射到 Tailwind 的 theme 对象中,但 是指向 CSS 变量的引用。

前置准备:确认工具函数

我们需要确保 src/utils/theme.ts 中包含 createTailwinConfgcreatColorChannel 这两个关键函数(您已提供源码)。

文件路径: src/utils/theme.ts (确认包含以下函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import color from 'color';
// 导入我们定义 Tokens 结构的文件 (用于 getThemeTokenVariants)
import { themeTokens } from '@/theme/type';
// (确保 AddChannelToLeaf 类型也已定义或导入)
// ... (rgbAlpha 函数) ...

/**
* 将 Tokens 路径转换为 CSS 变量名
* @example toCssVar('colors.palette.primary.default') // '--colors-palette-primary-default'
*/
export const toCssVar = (propertyPath: string) => {
// 注意:这里可能需要添加 VE 生成的 hash 后缀,或者 VE 插件能处理?
// 简化版:假设 VE 生成的变量名是可预测的 (无 hash 或 hash 规则已知)
// 这是一个需要根据实际 VE 输出调整的关键点
return `--${propertyPath.split(".").join("-")}`;
// 更健壮的方式是直接导入 themeVars 对象,但这可能导致循环依赖
// 或者在构建时生成一个映射文件
};

/**
* 获取指定路径下 Tokens 的所有变体名 (叶子节点的 key)
* @example getThemeTokenVariants('colors.palette.primary') // ['lighter', 'light', 'default', 'dark', 'darker']
*/
export const getThemeTokenVariants = (propertyPath: string): string[] => {
const keys = propertyPath.split(".");
let current: any = themeTokens; // 从 Tokens 骨架开始查找
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
console.warn(`[getThemeTokenVariants] Path not found: ${propertyPath}`);
return []; // 未找到路径,返回空数组
}
}
// 确保最终找到的是一个对象且其值都是 null (表示叶子节点集合)
if (current && typeof current === 'object' && Object.values(current).every(v => v === null)) {
return Object.keys(current);
}
console.warn(`[getThemeTokenVariants] Path does not point to a leaf object: ${propertyPath}`);
return [];
};


/**
* 为 Tailwind theme 配置生成 CSS 变量引用
* @example createTailwinConfg('spacing')
* // returns { '0': 'var(--spacing-0)', '1': 'var(--spacing-1)', ... }
*/
export const createTailwinConfg = (propertyPath: string) => {
const variants = getThemeTokenVariants(propertyPath);
const result = variants.reduce(
(acc, variant) => {
// 生成 Tailwind theme key (可能需要处理数字 key,如 'spacing')
const themeKey = Number.isNaN(Number(variant)) ? variant : `${variant}`;
// 生成指向 VE CSS 变量的引用
acc[themeKey] = `var(${toCssVar(`${propertyPath}.${variant}`)})`; // 注意路径拼接
return acc;
},
{} as Record<string, string>,
);
return result;
};

/**
* 为 Tailwind theme.colors 配置生成 rgb(var(...Channel)) 引用
* 支持颜色透明度 (e.g., bg-primary/50)
* @example creatColorChannel('colors.palette.primary')
* // returns {
* // DEFAULT: 'rgb(var(--colors-palette-primary-defaultChannel))',
* // lighter: 'rgb(var(--colors-palette-primary-lighterChannel))', ...
* // }
*/
export const creatColorChannel = (propertyPath: string) => {
const variants = getThemeTokenVariants(propertyPath);
const result = variants.reduce(
(acc, variant) => {
// Tailwind 需要大写的 DEFAULT 作为默认 key
const variantKey = variant === "default" ? "DEFAULT" : variant;
// 生成指向 VE Channel CSS 变量的 rgb() 引用
acc[variantKey] = `rgb(var(${toCssVar(`${propertyPath}.${variant}Channel`)}))`; // 注意路径和 Channel 后缀
return acc;
},
{} as Record<string, string>,
);
return result;
};

// ... (removePx, addColorChannels 函数) ...

配置 tailwind.config.ts

现在,我们利用这些工具函数来填充 tailwind.config.ts

文件路径: tailwind.config.ts (完整配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import type { Config } from "tailwindcss";
import { breakpointsTokens } from "./src/theme/tokens/breakpoints";
// 导入工具函数和 tokens
import {
createColorChannel,
createTailwinConfg,
} from "./src/theme/utils/themeUtils";

export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],

theme: {
// 映射非颜色的、可覆盖的 theme 属性
fontFamily: createTailwinConfg("typography.fontFamily"),

// 映射需要扩展的 theme 属性
extend: {
colors: {
// 语义化颜色
primary: {
...createColorChannel("colors.palette.primary"),
foreground: "rgb(var(--colors-common-white-channel))",
},
destructive: {
...createColorChannel("colors.palette.error"),
foreground: "rgb(var(--colors-common-white-channel))",
},
secondary: {
DEFAULT: "rgb(var(--colors-palette-primary-default-channel) / 0.1)",
foreground: "rgb(var(--colors-palette-primary-default-channel))",
},
accent: {
DEFAULT: "rgb(var(--colors-background-neutral-channel))",
foreground: "rgb(var(--colors-text-primary-channel))",
},

// 辅助颜色
success: createColorChannel("colors.palette.success"),
warning: createColorChannel("colors.palette.warning"),
error: createColorChannel("colors.palette.error"),
info: createColorChannel("colors.palette.info"),
gray: createColorChannel("colors.palette.gray"),

// 基础颜色
background: {
DEFAULT: "rgb(var(--colors-background-default-channel))",
paper: "rgb(var(--colors-background-paper-channel))",
},
foreground: "rgb(var(--colors-text-primary-channel))",

// 边框和其他
border: "rgb(var(--colors-palette-gray-300-channel))",
input: "rgb(var(--colors-palette-gray-300-channel))",
ring: "rgb(var(--colors-palette-primary-default-channel))",
},
opacity: createTailwinConfg("opacity"),
borderRadius: createTailwinConfg("borderRadius"),
boxShadow: createTailwinConfg("shadows"),
spacing: createTailwinConfg("spacing"),
zIndex: createTailwinConfg("zIndex"),

// 意图:对于断点,我们不需要 var() 引用,而是直接使用其值。
// 因此直接导入并赋值即可。
screens: breakpointsTokens,
},
},
plugins: [],
} satisfies Config;

关键配置解析

  • darkMode: 配置为 ["selector", "[data-theme-mode='dark']"],与 CSS 中的 @variant dark 以及 ThemeProvider 的行为完全一致。
  • content: 必须包含所有可能使用 Tailwind 类名的文件路径,否则 Tailwind 无法生成对应的 CSS。
  • theme & theme.extend: 这是核心。我们 没有 在这里写死任何具体值(如 #00b96b16px),而是全部通过 createTailwinConfgcreatColorChannel 动态生成了指向我们 Vanilla Extract CSS 变量的引用 (var(...)rgb(var(...)))。
  • 工具函数的作用:
    • createTailwinConfg: 用于生成简单的 var(--variable-name) 引用,适用于 spacing, borderRadius, fontFamily 等。
    • creatColorChannel: 专门用于颜色,生成 rgb(var(--variable-nameChannel)) 引用,使得 Tailwind 的颜色透明度修饰符(如 bg-primary/50)能够正确工作。
  • Shadcn/UI 映射: 直接将 Tailwind 的 background, foreground 等 key 映射到我们在 index.css 中定义的 CSS 变量别名 (var(--background)),提供了良好的兼容性。

6.4.3. 第三步:最终验证

配置完成后,最重要的一步是验证 Tailwind 是否真正地消费了我们的主题系统,并且能够响应主题切换。

第一步:重启开发服务器
确保 vite.config.ts, tailwind.config.ts, src/index.css 的修改都被加载。

第二步:在 MyApp.tsx 中使用映射后的 Tailwind 类

文件路径: src/MyApp.tsx (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import { App as AntdApp, Button, Divider, Space, Typography } from "antd";
// 导入 Zustand actions (假设已导出)
import { useSettingActions } from "@/store/settingStore";
import { ThemeMode, ThemeColorPresets } from "@/types/enum"; // 导入枚举

function MyApp() {
const { setSettings } = useSettingActions(); // 获取更新函数

const toggleThemeMode = () => {
setSettings({
themeMode: document.documentElement.getAttribute(HtmlDataAttribute.ThemeMode) === ThemeMode.Light
? ThemeMode.Dark
: ThemeMode.Light,
});
};

const changeColorPreset = (color: ThemeColorPresets) => {
setSettings({ themeColorPresets: color });
};

return (
// 使用映射后的 Tailwind 类:bg-bg-default (来自 VE background.default)
<div className="flex flex-col items-center justify-center min-h-screen bg-bg-default p-8 transition-colors duration-300">
{/* 卡片:使用 bg-bg-paper, rounded-lg (来自 VE borderRadius.lg), shadow-card (来自 VE shadows.card) */}
<div className="w-full max-w-lg p-8 space-y-6 bg-bg-paper rounded-lg shadow-card">
{/* 标题:使用 text-primary (来自 VE colors.palette.primary.default) */}
<h1 className="text-2xl font-bold text-center text-primary">
Prorise-Admin
</h1>

{/* 使用 Antd Typography (样式应与原生 h1 一致) */}
<div className="text-center">
<Typography.Title level={2} className="text-text-primary">Antd Title (h2)</Typography.Title>
{/* 使用 text-text-secondary */}
<Typography.Text className="text-text-secondary">Antd Text</Typography.Text>
</div>

{/* 使用 Antd Button (其 primary 颜色应与 text-primary 一致) */}
<div className="flex justify-center">
<Button type="primary">Antd Button</Button>
</div>

<Divider className="border-border" /> {/* 使用 border-border (来自 shadcn 映射) */}

{/* 测试主题切换按钮 */}
<Space wrap className="flex justify-center">
<Button onClick={toggleThemeMode}>Toggle Light/Dark</Button>
<Button onClick={() => changeColorPreset(ThemeColorPresets.Blue)}>Set Blue Preset</Button>
<Button onClick={() => changeColorPreset(ThemeColorPresets.Default)}>Set Default Preset</Button>
</Space>

{/* 测试间距:使用 p-4 (来自 VE spacing.4) */}
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-md">
{/* 测试字体:使用 font-openSans (来自 VE typography.fontFamily.openSans) */}
<p className="font-openSans text-text-primary">This uses Open Sans font.</p>
{/* 测试响应式:只在 lg 断点及以上显示 (来自 VE screens.lg) */}
<p className="hidden lg:block text-text-secondary">Only visible on large screens.</p>
</div>
</div>
</div>
);
}

export default MyApp;

验证要点

  1. 颜色一致性h1 的颜色 (text-primary)、Antd Button 的主色、以及 border-border 的颜色,是否都正确地反映了当前的主题色(默认是绿色)?
  2. 间距/圆角/字体/断点p-4, rounded-lg, font-openSans, lg:block 这些基于 Tokens 映射的 Tailwind 类是否按预期工作?
  3. 亮/暗模式切换:点击 “Toggle Light/Dark” 按钮,整个页面的背景 (bg-bg-default)、卡片背景 (bg-bg-paper)、文字颜色 (text-text-primary/secondary) 是否流畅地切换?Tailwind 的 dark: 前缀是否生效(例如 dark:bg-gray-800)?
  4. 主题色切换:点击 “Set Blue Preset” 按钮,h1 (text-primary) 和 Antd Button 的主色是否都变成了蓝色?点击 “Set Default Preset” 是否能切换回绿色?

如果以上所有验证都通过,那么恭喜你!我们已经成功地让 Tailwind CSS 完全融入了我们的主题系统,实现了 Ant Design 与 Tailwind 视觉风格的完美统一和动态响应。

阶段性成果:我们成功配置了 Tailwind CSS v4,使其能够 消费 由 Vanilla Extract 生成的 CSS 变量。通过在 CSS 中定义 darkMode 策略,并在 tailwind.config.ts 中利用工具函数 映射 Design Tokens 结构(值为 var(...) 引用),我们确保了 Tailwind 原子类与 Ant Design 组件在视觉上保持 完全一致,并且都能 动态响应 亮/暗模式和主题色的切换。混合样式架构的关键协同机制已经打通。


6.4. 配置 Tailwind 消费 Tokens:实现原子化样式的统一

我们已经成功地为 Ant Design 这座“城市”修建了接入主题高速公路的“匝道”(AntdAdapter)。现在,轮到我们庞大的“乡村公路网络”——Tailwind CSS 了。

我们的目标是让每一个原子化类(如 bg-primary, p-4)都精确地行驶在我们的主题“高速公路”上,确保所有自定义布局和组件的视觉表现,都与我们的 Design Tokens 保持绝对一致。

本章的核心目标:我们将对 Tailwind CSS 进行一次精密的“配置连接”,将其默认的“引擎”和“燃料”全部替换掉。我们将配置它,使其 不再使用任何内置的默认值,而是 完全依赖并消费 我们通过 Vanilla Extract 生成的全局 CSS 变量。

6.4.1. 核心策略:让 Tailwind 成为 CSS 变量的“消费者”

在深入配置之前,我们必须明确我们的核心指导思想。策略非常简单,但威力巨大:

我们将修改 tailwind.config.ts,让 theme 对象中的每一个值(颜色、间距、圆角等),都变成一个指向我们 CSS 变量的引用(例如 var(--colors-palette-primary-default-value))。

这样一来,Tailwind JIT 编译器在扫描到 text-primary 这样的类时,生成的 CSS 不再是 color: #some-hardcoded-color;,而是 color: rgb(var(--colors-palette-primary-default-channel));

其结果是:当 ThemeProvider 通过修改 <html> 属性来切换 CSS 变量的值时,所有由 Tailwind 生成的样式都会 自动地、响应式地 更新,因为它们引用的“源头”已经改变了。这就是我们打通 Tailwind 与主题系统协同工作的关键所在。

6.4.2. 第一步:配置 Tailwind 的暗黑模式“开关”

我们要做的第一件事,是告诉 Tailwind 如何识别“现在是暗黑模式”。我们的 ThemeProvider 会在 <html> 元素上添加 data-theme-mode="dark" 属性。我们需要让 Tailwind 的 dark: 变体能够识别这个“信号”。

在 Tailwind v4 中,推荐在 CSS 文件中定义这个策略。

文件路径: src/index.css (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 依旧保留 tailwindcss 的完整导入 */
@import "tailwindcss";
/* pnpm add -D tw-animate-css */
@import "tw-animate-css";

/* --- 关键变更 1: 声明配置文件路径 --- */
/* 
  意图:Tailwind v4 不再自动寻找配置文件。
  这行代码是必须的,它明确告诉 Tailwind 去哪里加载我们的 JS 配置。
*/
@config "../tailwind.config.ts";

/* ... (@layer base 的覆盖可以暂时保持不变) ... */

/* --- 关键变更 2: 定义暗黑模式变体 --- */
/*
  意图:这就是我们的“开关”。这行代码创建了一个名为 `dark` 的变体。
  它的工作逻辑是:当一个元素能够匹配 `:is([data-theme-mode="dark"] *)` 这个选择器时
  (即,当它的祖先元素,通常是 <html>,拥有 data-theme-mode="dark" 属性时),
  所有以 `dark:` 为前缀的工具类就会被激活。
  这完美地与我们 ThemeProvider 的行为对接。
*/
@variant dark (&:is([data-theme-mode="dark"] *));

思路落地:通过这两行简单的配置,我们已经完成了最关键的一步连接。Tailwind 的 dark: 变体现在已经和我们的 ThemeProvider 状态同步,为后续的动态样式切换奠定了基础。

6.4.3. 第二步:构建“Token-to-Tailwind”的自动化映射工具

接下来,我们需要填充 tailwind.config.ts。如果我们手动去写 'primary': 'var(--...)' 这样的映射,会非常繁琐且容易出错。因此,我们需要创建一套自动化工具函数,来读取我们的 Tokens 结构,并生成对应的 Tailwind 配置。

1. 工具 A: getThemeTokenVariants - 获取 Token 变体 (健壮版)

第一个工具需要能够读取我们定义的 themeTokens 结构,并返回指定路径下的所有“叶子节点”的 key。

关键:这个函数必须足够“聪明”,既能识别像 spacing 这样的 null 叶子,也能识别像 colors.palette.primary 这样的 { value: null, channel: null } 叶子。

文件路径: src/theme/utils/themeUtils.ts (添加新函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 导入我们定义的 Tokens 骨架,作为查询的依据
import { themeTokens } from '@/theme/type';

/**
 * 获取指定路径下 Tokens 的所有变体名 (叶子节点的 key)
 * @param propertyPath e.g., "spacing" or "colors.palette.primary"
 */
export const getThemeTokenVariants = (propertyPath: string): string[] => {
const keys = propertyPath.split(".");
let current: Record<string, unknown> = themeTokens;
for (const key of keys) {
if (current && typeof current === "object" && key in current) {
current = current[key] as Record<string, unknown>;
} else {
console.warn(`[getThemeTokenVariants] Path not found: ${propertyPath}`);
return []; // 路径无效,返回空数组
}
}

// 确认最终找到的是一个叶子节点对象
if (current && typeof current === "object") {
const values = Object.values(current);

// 检查 1: 是否为标准叶子 (e.g., spacing) - 所有值都是 null
const isStandardLeaf = values.every((v) => v === null);

// 检查 2: 是否为颜色叶子 (e.g., colors.palette.primary)
// - 所有值都是 { value: null, channel: null }
const isColorLeaf = values.every(
(v) => v && typeof v === "object" && "value" in v && v.value === null,
);

// 只要满足任一条件,就返回 keys
if (isStandardLeaf || isColorLeaf) {
return Object.keys(current);
}
}

console.warn(
`[getThemeTokenVariants] Path does not point to a leaf object: ${propertyPath}`,
);
return [];
};

意图解析:这个健壮的 getThemeTokenVariants 是后续所有映射的基础。通过同时检查两种叶子节点结构,它能正确地为 spacingcolors.palette.primary 返回它们的 keys,而不会像天真的实现那样在遇到颜色时返回 []

2. 工具 B: toCssVar - 拼接 CSS 变量名

这个工具很简单,负责将一个点分隔的路径(如 colors.palette.primary.default)转换成一个标准的 CSS 变量名(--colors-palette-primary-default)。

文件路径: src/theme/utils/themeUtils.ts (添加新函数)

1
2
3
4
5
6
/**
* 将 Tokens 路径转换为 CSS 变量名
*/
export const toCssVar = (propertyPath: string) => {
return `--${propertyPath.split(".").join("-")}`;
};

3. 工具 C: createTailwinConfg - 生成标准 var() 引用

这个工具将组合前两个工具,为间距、圆角等 非颜色 Token 生成 Tailwind 配置。

文件路径: src/theme/utils/themeUtils.ts (添加新函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 为 Tailwind theme 配置生成 CSS 变量引用
* @example createTailwinConfg('spacing')
* // returns { '0': 'var(--spacing-0)', '1': 'var(--spacing-1)' }
*/
export const createTailwinConfg = (propertyPath: string) => {
const variants = getThemeTokenVariants(propertyPath);
return variants.reduce(
(acc, variant) => {
// Vanilla Extract 会直接使用 key 作为变量名 (e.g., --spacing-0)
acc[variant] = `var(${toCssVar(`${propertyPath}.${variant}`)})`;
return acc;
},
{} as Record<string, string>,
);
};

4. 工具 D: createColorChannel - 为颜色生成特殊引用 (修正版)

颜色比较特殊。为了让 Tailwind 的 透明度修饰符(如 bg-primary/50)能正常工作,我们必须引用 Channel 变量,并用 rgb() 包裹。

关键:我们必须正确拼接由 Vanilla Extract 生成的 -channel 后缀。

文件路径: src/theme/utils/themeUtils.ts (添加新函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 为颜色生成 rgb(var(...-channel)) 引用,以支持透明度
 * @example createColorChannel('colors.palette.primary')
 * // returns { DEFAULT: 'rgb(var(--colors-palette-primary-default-channel))', ... }
 */
export const createColorChannel = (propertyPath: string) => {
  // 1. 感谢健壮的 getThemeTokenVariants,这里能正确获取到 ['lighter', 'light', ...]
  const variants = getThemeTokenVariants(propertyPath);

  return variants.reduce(
    (acc, variant) => {
      const variantKey = variant === "default" ? "DEFAULT" : variant; // Tailwind 的特殊约定

      // 2. 生成基础变量名, e.g., --colors-palette-primary-default
      const cssVar = toCssVar(`${propertyPath}.${variant}`);
      
      // 3. [关键] 拼接 '-channel' 后缀,
// 这对应了 { value: null, channel: null } 契约的 'channel' 键
      acc[variantKey] = `rgb(var(${cssVar}-channel))`;
      return acc;
    },
    {} as Record<string, string>,
  );
};

思路落地:我们已经打造了一套完整的、职责清晰的、且 逻辑正确 的自动化工具。现在,填充 tailwind.config.ts 将会变得极其简单和清晰。

6.4.4. 第三步:分部组装 tailwind.config.ts

现在,我们利用刚刚创建的工具箱,一步步地构建我们的 tailwind.config.ts 文件。

1. 基础配置:content

首先,我们创建文件并配置最基本的部分。

文件路径: tailwind.config.ts (新建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type { Config } from "tailwindcss";
export default {
// 意图:告诉 Tailwind 去扫描哪些文件,以确定需要生成哪些 CSS 类。
// 路径不全会导致样式丢失。
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],

// 意图:我们【故意省略】了 'darkMode' 键。
// 在 Tailwind v4 的 CSS-First 架构中,我们已经在 src/index.css 中
// 通过 @variant dark (...) 声明了暗黑模式策略。
// 这确保了 index.css 是配置的“唯一事实来源”,避免了 JS 和 CSS 配置的冲突。

theme: {
// 我们将在这里逐步填充
extend: {},
},
plugins: [],
} satisfies Config;

2. 填充 theme.extend.colors

接下来,使用 createColorChannel 工具来映射我们所有的颜色 Token。

文件路径: tailwind.config.ts (修改 theme 部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import type { Config } from "tailwindcss";
import { createColorChannel } from "./src/theme/utils/themeUtils";
export default {
// ....
theme: {
extend: {
colors: {
primary: createColorChannel("colors.palette.primary"),
success: createColorChannel("colors.palette.success"),
warning: createColorChannel("colors.palette.warning"),
error: createColorChannel("colors.palette.error"),
info: createColorChannel("colors.palette.info"),
gray: createColorChannel("colors.palette.gray"),
text: createColorChannel("colors.text"),
// 注意:Tailwind 习惯用 bg 作为背景色 key,
// 我们将其映射到 tokens 中的 background。
bg: createColorChannel("colors.background"),
// 'action' 也是颜色(包含 rgba 值),同样使用 channel 映射
action: createColorChannel("colors.action"),
},
},
},
plugins: [],
} satisfies Config;

意图解析:现在,当你在代码中使用 bg-primarybg-primary/50,Tailwind 生成的 CSS 将是 background-color: rgb(var(--colors-palette-primary-default-channel) / 0.5),完全由我们的主题系统驱动。

3. 填充其他 Tokens

最后,我们使用 createTailwinConfg 和直接导入的方式,完成剩余所有 Token 的映射。

文件路径: tailwind.config.ts (最终版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import type { Config } from "tailwindcss";
import { breakpointsTokens } from "./src/theme/tokens/breakpoints";
// 导入工具函数和 tokens
import {
createColorChannel,
createTailwinConfg,
} from "./src/theme/utils/themeUtils";

export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],

theme: {
// 映射非颜色的、可覆盖的 theme 属性
fontFamily: createTailwinConfg("typography.fontFamily"),

// 映射需要扩展的 theme 属性
extend: {
colors: {
primary: createColorChannel("colors.palette.primary"),
success: createColorChannel("colors.palette.success"),
warning: createColorChannel("colors.palette.warning"),
error: createColorChannel("colors.palette.error"),
info: createColorChannel("colors.palette.info"),
gray: createColorChannel("colors.palette.gray"),
text: createColorChannel("colors.text"),
bg: createColorChannel("colors.background"),
action: createColorChannel("colors.action"),
},
opacity: createTailwinConfg("opacity"),
borderRadius: createTailwinConfg("borderRadius"),
boxShadow: createTailwinConfg("shadows"),
spacing: createTailwinConfg("spacing"),
zIndex: createTailwinConfg("zIndex"),

// 意图:对于断点,我们不需要 var() 引用,而是直接使用其值。
// 因此直接导入并赋值即可。
screens: breakpointsTokens,
},
},
plugins: [],
} satisfies Config;

思路落地:我们的 tailwind.config.ts 已经组装完毕。它现在是一个纯粹的“声明文件”,声明了 Tailwind 主题与我们 Design Tokens CSS 变量之间的映射关系,没有任何硬编码的样式值,并且 逻辑正确

6.4.5. 第四步:最终验证

理论必须通过实践来检验。我们将通过修改 MyApp.tsx 来验证我们的配置是否完美生效。

文件路径: src/MyApp.tsx (修改)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import { Button as AntButton, Card as AntCard, Space } from "antd";
import { useSettingActions, useSettings } from "@/store/settingStore";
import { ThemeColorPresets, ThemeMode } from "@/theme/types/enum";

function MyApp() {
const settings = useSettings();
const { setSettings } = useSettingActions();

// 切换亮暗模式
const toggleThemeMode = () => {
setSettings({
themeMode:
settings.themeMode === ThemeMode.Light
? ThemeMode.Dark
: ThemeMode.Light,
});
};

// 切换主题预设
const changeColorPreset = (preset: ThemeColorPresets) => {
setSettings({ themeColorPresets: preset });
};

return (
<div className="min-h-screen bg-bg-default p-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* 标题 */}
<h1 className="text-4xl font-bold text-text-primary text-center mb-12">
主题系统测试页面
</h1>

{/* 控制面板 */}
<div className="flex flex-col items-center gap-6 p-6 bg-bg-paper rounded-lg shadow-card">
<h2 className="text-2xl font-semibold text-text-primary">主题控制</h2>

{/* 亮暗模式切换 */}
<div className="flex items-center gap-4">
<span className="text-text-secondary">当前模式:</span>
<button
type="button"
onClick={toggleThemeMode}
className="px-6 py-2 bg-primary text-white rounded-lg hover:opacity-90 transition-opacity shadow-primary"
>
{settings.themeMode === ThemeMode.Light
? "🌞 切换到暗色"
: "🌙 切换到亮色"}
</button>
</div>

{/* 主题预设切换 */}
<div className="flex flex-col items-center gap-3">
<span className="text-text-secondary">选择主题色:</span>
<div className="flex flex-wrap gap-3 justify-center">
{Object.values(ThemeColorPresets).map((preset) => (
<button
key={preset}
type="button"
onClick={() => changeColorPreset(preset)}
className={`px-4 py-2 rounded-lg transition-all ${
settings.themeColorPresets === preset
? "bg-primary text-white shadow-lg scale-105"
: "bg-bg-neutral text-text-primary hover:bg-action-hover"
}`}
>
{preset.charAt(0).toUpperCase() + preset.slice(1)}
</button>
))}
</div>
</div>
</div>

{/* 卡片对比 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Tailwind CSS 卡片 */}
<div className="space-y-4">
<h3 className="text-xl font-semibold text-text-primary text-center">
Tailwind CSS 卡片
</h3>
<div className="bg-bg-paper rounded-lg shadow-card p-6 space-y-4">
<h4 className="text-lg font-semibold text-primary">
使用 Tailwind 构建
</h4>
<p className="text-text-secondary">
这个卡片使用了我们自定义的 Tailwind CSS
类名,包括颜色、间距、圆角和阴影。
</p>

{/* 颜色展示 */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary rounded"></div>
<span className="text-text-primary text-sm">Primary</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-success rounded"></div>
<span className="text-text-primary text-sm">Success</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-warning rounded"></div>
<span className="text-text-primary text-sm">Warning</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-error rounded"></div>
<span className="text-text-primary text-sm">Error</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-info rounded"></div>
<span className="text-text-primary text-sm">Info</span>
</div>
</div>

{/* 按钮示例 */}
<div className="flex gap-3 flex-wrap">
<button
type="button"
className="px-4 py-2 bg-primary text-white rounded hover:opacity-90"
>
Primary
</button>
<button
type="button"
className="px-4 py-2 bg-success text-white rounded hover:opacity-90"
>
Success
</button>
<button
type="button"
className="px-4 py-2 bg-warning text-white rounded hover:opacity-90"
>
Warning
</button>
<button
type="button"
className="px-4 py-2 bg-error text-white rounded hover:opacity-90"
>
Error
</button>
</div>
</div>
</div>

{/* Ant Design 卡片 */}
<div className="space-y-4">
<h3 className="text-xl font-semibold text-text-primary text-center">
Ant Design 卡片
</h3>
<AntCard
title="使用 Ant Design 构建"
bordered={false}
style={{ boxShadow: "var(--shadows-card)" }}
>
<p style={{ marginBottom: 16 }}>
这个卡片使用了 Ant Design 组件,通过 AntdAdapter
自动应用了我们的主题令牌。
</p>

<Space direction="vertical" style={{ width: "100%" }}>
<AntButton type="primary" block>
Primary Button
</AntButton>
<AntButton type="default" block>
Default Button
</AntButton>
<AntButton type="dashed" block>
Dashed Button
</AntButton>
<AntButton type="text" block>
Text Button
</AntButton>
<AntButton type="link" block>
Link Button
</AntButton>
</Space>

<Space style={{ marginTop: 16 }} wrap>
<AntButton type="primary">Primary</AntButton>
<AntButton type="primary" danger>
Danger
</AntButton>
<AntButton disabled>Disabled</AntButton>
</Space>
</AntCard>
</div>
</div>

{/* 状态显示 */}
<div className="bg-bg-paper p-6 rounded-lg shadow-card">
<h3 className="text-xl font-semibold text-text-primary mb-4">
当前主题配置
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-text-secondary">
<div>
<span className="font-medium">主题模式:</span>{" "}
{settings.themeMode}
</div>
<div>
<span className="font-medium">主题预设:</span>{" "}
{settings.themeColorPresets}
</div>
<div>
<span className="font-medium">字体:</span> {settings.fontFamily}
</div>
<div>
<span className="font-medium">基础字号:</span> {settings.fontSize}
px
</div>
</div>
</div>
</div>
</div>
);
}

export default MyApp;

启动你的应用,并逐一核对以下清单

  1. 初始渲染:页面背景、卡片背景、标题颜色、字体是否都正确应用了我们 Tokens 中定义的默认(亮色)主题?bg-bg-default 是否生效?
  2. 切换暗黑模式:点击按钮,背景、卡片、文字颜色是否都平滑地过渡到了暗黑模式的对应值?dark:bg-gray-800 是否生效?
  3. 切换主题色:点击 “Set Blue Preset” 按钮,text-primary 和 Ant Design 的主按钮颜色是否 同时、同步 地改变?点击 “Set Default Preset” 是否能切换回绿色?
  4. 检查开发者工具:审查 text-primary 的元素,它的 color 样式是否为 rgb(var(--colors-palette-primary-default-channel))?(注意defaultchannel 之间有破折号)。

如果所有问题的答案都是“是”,那么,我们已经取得了里程碑式的成功。

阶段性成果:我们通过 构建健壮的自动化工具精简的配置文件,成功地将 Tailwind CSS 的主题系统与我们的 Design Tokens CSS 变量进行了深度绑定。现在,Tailwind 不再是一个独立的样式孤岛,而是我们统一主题生态系统中的一个强大、可动态响应的“消费者”。Ant Design 和 Tailwind 真正实现了视觉上的“同源”和行为上的“同步”。


6.5. 本章小结

在本章中,我们完成了从“静态蓝图”到“动态系统”的终极飞跃。我们为 Prorise-Admin 注入了“灵魂”——一个由状态驱动、响应式的、统一的主题引擎。

回顾本章,我们取得了四大关键成就,构建了一个完整的主题驱动闭环:

  1. “大脑”:Zustand 状态管理 (6.1)

    • 我们引入了轻量级的 zustand,并创建了 settingStore 作为所有主题设置的“唯一事实来源”。
    • 我们遵循了“渐进式构建”的最佳实践,从一个最小化的 Store 演进到支持 Partial 更新的、按 settingsactions 命名空间隔离的优雅结构。
    • 最重要的是,我们集成了 persist 中间件,并使用 partialize 进行了关键优化,实现了状态的本地持久化。
    • 我们 统一了类型定义,将 StorageEnum 添加到全局唯一的 src/theme/types/enum.ts 文件中,确保了类型安全且无冲突。
  2. “中枢神经”:ThemeProvider (6.2)

    • 我们构建了 ThemeProvider 组件,它充当了“状态”与“DOM”之间的中枢神经。
    • 它订阅 useSettings Hook,并使用 useEffect 将 Zustand 中的状态(themeMode, themeColorPresets 等)实时同步<html> 根元素的 data-* 属性和 style 属性上。
    • 这一步 激活 了我们在第五章中创建的所有 CSS 变量,使亮/暗模式和主题色切换成为了现实。
  3. “专属翻译官”:Ant Design 适配器 (6.3)

    • 我们遇到了一个核心架构问题:Ant Design 的 ConfigProvider “听不懂” CSS 变量。
    • 我们 摒弃了“紧耦合”的错误设计,而是采用了优雅的 适配器模式 (Adapter Pattern)
    • 我们创建了 AntdAdapter,它作为一个独立的“翻译官”,订阅 useSettings 状态,并将其“翻译”为 Antd ConfigProvider 所需的 theme 对象。
    • 我们升级了 ThemeProvider 使其支持 adapters 数组,实现了 对扩展开放、对修改关闭 的架构原则。
  4. “统一网络”:Tailwind CSS 消费 (6.4)

    • 这是打通混合架构的“最后一公里”。我们通过 健壮的自动化工具,将 Tailwind CSS 彻底转变为我们主题系统的“纯消费者”。
    • 我们 创建了 getThemeTokenVariants 工具,使其能智能识别 null{ value, channel } 两种契约结构。
    • 我们 创建了 createColorChannel 工具,使其能正确拼接 -channel 后缀,以引用正确的 CSS 变量。
    • 我们 精简了配置,在 index.css 中使用 @variant dark 定义暗黑模式,移除了 tailwind.config.ts 中的冗余 darkMode 键,实现了纯粹的 CSS-First 配置。

最终成果:我们拥有了一个由 Zustand 单一状态源驱动的、可动态切换、可持久化的主题系统。这个系统能够同时、同步地驱动我们的 原生 CSS 变量Ant Design 组件Tailwind CSS 原子类,实现了三位一体的完美视觉统一。


6.6. 代码入库:动态主题系统闭环

我们已经完成了 Prorise-Admin 核心基础设置中最复杂、最关键的一章。我们构建了一个完整的、从状态管理到 UI 适配的动态主题系统。是时候将这个巨大的里程碑存入我们的 Git 历史了。

第一步:检查代码状态

使用 git status 查看我们海量的变更。这可能是我们迄今为止最大的一次提交。

1
git status

你会看到一系列的新增和修改:

  • 依赖package.json / pnpm-lock.yaml (新增 zustand)。
  • 状态src/store/settingStore.ts (新目录和文件)。
  • 类型src/theme/types/enum.ts (修改,新增 StorageEnum)。
  • Providersrc/theme/theme-provider.tsx (新增)。
  • 适配器src/theme/adapter/antd.adapter.tsx (新目录和文件),src/theme/type.ts (修改,新增 UILibraryAdapter 类型)。
  • 工具链src/utils/theme.ts (修改,新增 removePx 和所有 Tailwind 映射工具)。
  • Tailwindsrc/index.css (修改,新增 @config@variant),tailwind.config.ts (新增/修改,配置了所有映射)。
  • 入口src/main.tsx (修改,使用了 ThemeProviderAntdAdapter)。
  • 测试src/MyApp.tsx (修改,用于验证主题切换)。

第二步:暂存所有变更

将所有新文件和修改添加到暂存区。

1
git add .

第三步:执行提交

我们编写一条符合“约定式提交”规范的 Commit Message。feat 是最合适的类型,themestate 都是合适的 scope

1
git commit -m "feat(theme): implement dynamic theme with zustand, provider, and adapters"

这条消息准确地概括了我们的工作:使用 Zustand 实现了动态主题,并通过 Provider 和 Adapters 将其应用。

一个统一的系统: 我们的这次提交,标志着项目从“样式集成”阶段演进到了“样式驱动”阶段。Zustand 成为了驱动一切的“心脏”,而 ThemeProviderAntdAdaptertailwind.config.ts 构成了连接到所有 UI 末端的“动脉”。