Skyroc Web Kit
Theme

@skyroc/web-admin-theme

Web 管理端主题管理包 — Jotai 状态、React 组件、预设系统、副作用收口与 Ant Design 集成的完整解决方案

概述

@skyroc/web-admin-theme 是面向应用层的主题管理包,构建于 @skyroc/adapter-antd-theme 之上,提供:

  • 全局状态:单一 Jotai atom 作为 theme 唯一数据源,所有组件共享
  • useTheme hook:统一读写主题的入口,封装所有派生计算和 mutation
  • AntdProvider 组件:自动桥接 theme 状态与 Ant Design ConfigProvider,整合水印
  • ThemeEffect 组件:副作用统一收口(DOM class、CSS 滤镜、localStorage 缓存)
  • 预设系统:5 套开箱即用 JSON 预设,支持运行时切换
  • 初始化流程setupTheme 处理缓存读取、版本覆盖和 atom 初始化

架构

src/
├── antd/
│   ├── AntdProvider.tsx      ConfigProvider + App + Watermark 统一封装
│   ├── shared.ts             getAntdTheme():theme 状态 → Ant Design theme 配置
│   └── ui.ts                 message / modal / notification 单例管理
├── components/
│   ├── ThemeEffect.tsx        副作用组件(DOM、storage、滤镜、水印定时器)
│   ├── ThemeSchemaSwitch.tsx  light/dark/auto 切换按钮
│   └── ThemeSchemaSegmented.tsx 分段式主题选择控件
├── config/
│   └── default.ts            defaultThemeSettings(默认配置对象)
├── hooks/
│   └── use-theme.ts          themeSettingsAtom + themeUserNameAtom + useTheme()
├── presets/                  5 套预设 JSON 文件
│   ├── default.json
│   ├── dark.json
│   ├── azir.json
│   ├── compact.json
│   └── shadcn.json
├── types/
│   └── theme.d.ts            全局 Theme namespace 类型声明
├── utils/
│   ├── dark-mode.ts          toggleCssDarkMode / isDarkModeClass
│   ├── filters.ts            toggleAuxiliaryColorModes / clearAuxiliaryColorModes
│   └── settings.ts           mergeThemeSettings / getThemeColors / getDefaultThemeSettings
└── setup.ts                  setupTheme / defineThemeOverrides

安装

pnpm add @skyroc/web-admin-theme jotai antd

Peer dependenciesantd >= 6.0.0react >= 19.0.0jotai >= 2.0.0

快速上手

1. 初始化(应用入口)

main.tsxapp.tsx 中,于任何渲染发生之前调用:

import { setupTheme } from '@skyroc/web-admin-theme';
import { storagePrefix } from '@/utils/storage';

setupTheme({
  buildTime: BUILD_TIME,        // vite define 注入的构建时间戳
  overrides: {                  // 可选:新版本强制覆盖的配置项
    themeColor: '#6366F1'
  },
  storagePrefix
});

isProd 默认读取 !__DEV__storage 默认使用带 storagePrefix 的 localStorage。storagePrefix 可由宿主传入,未传时默认 SR_buildTime 仍由宿主传入,用于生产环境版本覆盖检测。开发环境直接使用默认配置;生产环境从 localStorage 读取缓存,并通过 buildTime 检测是否需要应用版本覆盖。


2. 挂载 AntdProvider

import { AntdProvider } from '@skyroc/web-admin-theme';
import { antdLocales } from '@/locales';

const AppProvider = ({ children }: { children: ReactNode }) => {
  const { locale } = useLocale();
  const { userInfo } = useAuth();

  return (
    <AntdProvider locale={antdLocales[locale]} userName={userInfo?.userName}>
      {children}
    </AntdProvider>
  );
};

AntdProvider 内部自动调用 useTheme(),将 theme 状态转换为 Ant Design ConfigProvider 配置,并将 userName 写入全局 atom 供水印使用。


3. 挂载 ThemeEffect

import { ThemeEffect } from '@skyroc/web-admin-theme';

const GlobalEffect = () => <ThemeEffect />;

ThemeEffect 渲染为 null,只负责副作用:监听 darkMode 变化写 DOM class、监听主题色变化存 storage、在页面卸载前持久化完整配置。storage 适配器由 setupTheme 统一注入,无需重复传入。


4. 在组件中使用 useTheme

import { useTheme } from '@skyroc/web-admin-theme';

const ThemePanel = () => {
  const {
    themeScheme,
    themeColor,
    darkMode,
    setThemeScheme,
    updateThemeColors,
    setThemeLayout,
    reset,
  } = useTheme();

  return (
    <div>
      <button onClick={() => setThemeScheme('dark')}>切换暗色</button>
      <button onClick={() => updateThemeColors('primary', '#6366F1')}>
        改主题色
      </button>
      <button onClick={() => setThemeLayout('horizontal')}>
        切换布局
      </button>
      <button onClick={reset}>重置</button>
    </div>
  );
};

5. 使用预设

import { getAllPresets, getPreset } from '@skyroc/web-admin-theme';
import { useTheme } from '@skyroc/web-admin-theme';

const { setSettings } = useTheme();

// 获取所有预设(已按 order 排序)
const allPresets = getAllPresets();

// 应用某个预设
const preset = getPreset('shadcn');
if (preset) {
  setSettings(preset);
}

6. Ant Design UI 工具函数

import {
  showSuccessMessage,
  showErrorMessage,
  showConfirmModal,
  showNotification
} from '@skyroc/web-admin-theme';

// 消息提示
showSuccessMessage('保存成功');
showErrorMessage('网络错误');

// 确认弹窗
showConfirmModal({
  title: '确认删除?',
  onOk: () => deleteItem(id)
});

// 通知
showNotification({ message: '任务完成', type: 'success' });

这些函数不依赖 React context,可在 axios 拦截器、事件处理器等任意位置调用。

API

setupTheme

function setupTheme(options?: SetupThemeOptions): void

interface SetupThemeOptions {
  /** 构建时间戳,用于检测是否需要应用版本覆盖 */
  buildTime?: string;
  /** 是否生产环境,默认 !__DEV__ */
  isProd?: boolean;
  /** 版本覆盖配置,新版本发布时强制应用到用户缓存上 */
  overrides?: Partial<Theme.ThemeSetting>;
  /** 存储适配器,需实现 get / set 方法;默认使用 localStorage */
  storage?: { get: (...args: any[]) => any; set: (...args: any[]) => void };
  /** 默认 localStorage 适配器的 key 前缀,默认 SR_ */
  storagePrefix?: string;
}

初始化逻辑:

  1. 开发环境:直接用 defaultThemeSettings 初始化 atom,不读 storage
  2. 生产环境
    • storage.get('themeSettings') 读缓存
    • mergeThemeSettings(cached, default) 合并(缓存优先)
    • 检查 overrideThemeFlag === buildTime:若未覆盖,则将 overrides 合并进来,并写入 flag
    • 将结果写入 themeSettingsAtom.init

defineThemeOverrides

类型安全的覆盖配置定义辅助,提供完整的 TypeScript 推导:

function defineThemeOverrides(overrides: Partial<Theme.ThemeSetting>): Partial<Theme.ThemeSetting>
// theme.config.ts
export const themeOverrides = defineThemeOverrides({
  themeColor: '#6366F1',
  themeRadius: 8,
  layout: { mode: 'horizontal', scrollMode: 'content' }
});

// main.tsx
setupTheme({ ..., overrides: themeOverrides });

useTheme

主题状态管理 hook,返回当前 theme 状态的所有字段 + 派生计算值 + mutation 函数:

function useTheme(): UseThemeReturn

读取值(来自 themeSettingsAtom 的展开):

字段类型说明
settingsTheme.ThemeSetting完整配置对象
themeSchemeThemeMode当前模式:'light' | 'dark' | 'auto'
themeColorstring主题色 hex
darkModeboolean当前是否处于暗色(auto 时跟随系统)
themeColorsThemeColor{ primary, info, success, warning, error }
grayscaleModeboolean灰度模式
colourWeaknessModeboolean色弱模式
watermarkContentstring计算后的水印文字
settingsJsonstring配置序列化字符串(用于比较变化)

mutation 函数:

函数签名说明
setSettings(update: Partial<Theme.ThemeSetting>) => void部分更新配置
setThemeScheme(mode: ThemeMode) => void设置亮/暗/自动
toggleThemeScheme() => void循环切换 light → dark → auto → light
updateThemeColors(key: ThemeColorKey, color: string) => void更新主题色(支持推荐色模式)
setThemeLayout(mode: ThemeLayoutMode) => void切换布局模式
setGrayscale(v: boolean) => void灰度模式开关
setColourWeakness(v: boolean) => void色弱模式开关
setWatermarkEnableUserName(v: boolean) => void水印显示用户名
setWatermarkEnableTime(v: boolean) => void水印显示时间
updateWatermarkTimer() => void同步水印定时器状态
reset() => void重置为默认配置

updateThemeColorssettings.recommendColor = true 时,会先通过推荐算法将输入色映射到最近 Tailwind 色系的 500 档,再写入配置。


themeSettingsAtom

const themeSettingsAtom: PrimitiveAtom<Theme.ThemeSetting>

Jotai atom,是 theme 状态的唯一数据源。在 useTheme 之外直接操作(如在 Jotai store 中批量更新):

import { themeSettingsAtom } from '@skyroc/web-admin-theme';
import { setAtomValue } from '@skyroc/core-state';

// 在非组件环境中直接写入
setAtomValue(themeSettingsAtom, newSettings);

themeUserNameAtom

const themeUserNameAtom: PrimitiveAtom<string | undefined>

存储水印用户名,由 AntdProvider 自动写入,useTheme 内部读取。消费者无需手动操作。


AntdProvider

interface AntdProviderProps {
  children: ReactNode;
  locale?: Locale;       // Ant Design 国际化 locale 对象
  userName?: string;     // 用户名(写入 themeUserNameAtom 供水印使用)
}

const AntdProvider: React.FC<AntdProviderProps>

内部实现:

<ConfigProvider theme={getAntdTheme(themeColors, darkMode, settings)} locale={locale}>
  <App>
    <ContextHolder />   ← 初始化 message/modal/notification 单例
    <Watermark content={watermarkContent} {...watermark.settings}>
      {children}
    </Watermark>
  </App>
</ConfigProvider>

getAntdTheme

将 theme 状态转换为 Ant Design ConfigProvider.theme 配置对象:

function getAntdTheme(
  colors: Theme.ThemeColor,
  darkMode: boolean,
  settings: Theme.ThemeSetting
): ConfigProviderProps['theme']

生成的配置包含:

  • algorithm[derivative][derivativeDark]
  • cssVar:启用 CSS 变量(prefix 为空,key 为 'root'
  • hashed: false:关闭 class hash,方便 CSS 覆盖
  • token:colorPrimary / colorError / fontSize / borderRadius 等基础 token
  • components:Button、Menu、Collapse 的默认样式调整

ThemeEffect

const ThemeEffect: React.FC

渲染为 null,无需任何 props。storage 适配器通过 setupTheme 注入,包内部统一使用,不向外暴露。管理以下副作用:

监听值副作用
darkModetoggleCssDarkMode(darkMode)<html> 加/去 "dark" class;写 storage darkMode
grayscaleMode / colourWeaknessModetoggleAuxiliaryColorModes()document.documentElement.style.filter
themeColors写 storage themeColor(主色 hex)
watermark.visible / enableTimeupdateWatermarkTimer() → 暂停 / 恢复水印时间定时器
beforeunload 事件写 storage themeSettings(仅生产环境)

ThemeSchemaSwitch

图标按钮,循环切换 light / dark / auto:

const ThemeSchemaSwitch: React.FC

点击调用 useTheme().toggleThemeScheme(),图标由 themeSchemeIcons 驱动。


ThemeSchemaSegmented

分段式选择控件,展示三个模式供用户直接选择:

const ThemeSchemaSegmented: React.FC

预设系统

getAllPresets

返回所有预设,按 order 字段升序排列:

function getAllPresets(): Theme.ThemePreset[]

getPreset

按名称查找预设:

function getPreset(name: PresetName): Theme.ThemePreset | undefined

type PresetName = 'default' | 'dark' | 'azir' | 'compact' | 'shadcn'

具名导出

import { defaultPreset, dark, azir, compact, shadcn, presets } from '@skyroc/web-admin-theme';

预设列表:

名称特点
default标准均衡配置,亮色模式,#646cff 主色
dark暗色模式优化,调整 token 色值
azir备选配色方案
compact紧凑间距,小字号,适合信息密集型界面
shadcnshadcn/ui 风格,中性主色,大圆角

每个预设是完整的 Theme.ThemePresetThemePresetMeta & Partial<ThemeSetting>),通过 setSettings(preset) 应用。


工具函数

mergeThemeSettings

深度合并两个 ThemeSetting,source 优先于 base

function mergeThemeSettings(
  source: Partial<Theme.ThemeSetting> | null | undefined,
  base: Theme.ThemeSetting
): Theme.ThemeSetting

getThemeColors

从 settings 提取 ThemeColor(处理 isInfoFollowPrimary 逻辑):

function getThemeColors(settings: Theme.ThemeSetting): ThemeColor

getDefaultThemeSettings

返回默认配置的深拷贝:

function getDefaultThemeSettings(): Theme.ThemeSetting

toggleCssDarkMode

切换 <html> 元素的 "dark" class:

function toggleCssDarkMode(darkMode: boolean): void

isDarkModeClass

检查当前 <html> 是否包含 "dark" class:

function isDarkModeClass(): boolean

toggleAuxiliaryColorModes

同步应用灰度和色弱 CSS 滤镜:

function toggleAuxiliaryColorModes(
  grayscale: boolean,
  colourWeakness: boolean
): void

灰度:filter: grayscale(100%);色弱:filter: invert(80%);同时开启叠加。

clearAuxiliaryColorModes

清除所有辅助颜色滤镜:

function clearAuxiliaryColorModes(): void

Ant Design UI 工具

通过 AntdProvider 内部的 ContextHolder 自动初始化,所有函数均可在 React 组件外调用:

// 消息
showMessage(config)
showSuccessMessage(content, duration?)
showErrorMessage(content, duration?)
showWarningMessage(content, duration?)
showInfoMessage(content, duration?)
showLoadingMessage(content, duration?)
destroyMessage()

// 弹窗
showModal(config)
showConfirmModal(config)
showSuccessModal(config)
showErrorModal(config)
showWarningModal(config)
showInfoModal(config)

// 通知
showNotification(config)
showSuccessNotification(config)
showErrorNotification(config)
showWarningNotification(config)
showInfoNotification(config)
destroyNotification()

类型参考

Theme.ThemeSetting

完整主题配置接口(全局 Theme namespace 内):

interface ThemeSetting {
  themeScheme: ThemeMode;             // 'light' | 'dark' | 'auto'
  themeColor: string;                 // 主色 hex
  themeRadius: number;                // 圆角基准值(默认 6)
  themeTextSize: number;              // 基准字号(默认 14)
  grayscale: boolean;                 // 灰度模式
  colourWeakness: boolean;            // 色弱模式
  recommendColor: boolean;            // 是否对输入色做推荐色映射
  isInfoFollowPrimary: boolean;       // info 色是否跟随主色
  otherColor: OtherColor;             // { info, success, warning, error }
  layout: {
    mode: ThemeLayoutMode;            // 7 种布局
    scrollMode: ThemeScrollMode;      // 'wrapper' | 'content'
  };
  fixedHeaderAndTab: boolean;
  header: {
    height: number;
    breadcrumb: { visible: boolean; showIcon: boolean };
    multilingual: { visible: boolean };
    globalSearch: { visible: boolean };
  };
  sider: {
    inverted: boolean;
    width: number;                    // 展开宽度(默认 220)
    collapsedWidth: number;           // 折叠宽度(默认 64)
    mixWidth: number;                 // mix 布局父级宽度
    mixCollapsedWidth: number;
    mixChildMenuWidth: number;
    autoSelectFirstMenu: boolean;
  };
  tab: {
    visible: boolean;
    cache: boolean;
    height: number;
    mode: ThemeTabMode;               // 'chrome' | 'button'
    closeTabByMiddleClick: boolean;
  };
  footer: {
    visible: boolean;
    fixed: boolean;
    height: number;
    right: boolean;
  };
  page: {
    animate: boolean;
    animateMode: ThemePageAnimateMode; // 7 种动画
  };
  watermark: {
    visible: boolean;
    text: string;
    enableCustomText: boolean;
    enableUserName: boolean;
    enableTime: boolean;
    timeFormat: string;
    settings: WatermarkSettings;      // Ant Design WatermarkProps
  };
  tokens: {
    light: ThemeSettingToken;
    dark?: Partial<ThemeSettingToken>;
  };
}

Theme.ThemeMode

type ThemeMode = 'auto' | 'dark' | 'light';

Theme.ThemeLayoutMode

7 种布局模式:

type ThemeLayoutMode =
  | 'vertical'                   // 左侧垂直菜单(默认)
  | 'horizontal'                 // 顶部水平菜单
  | 'horizontal-mix'             // 水平混合
  | 'reversed-horizontal-mix'    // 反转水平混合
  | 'vertical-mix'               // 双列垂直混合
  | 'top-hybrid-sidebar-first'   // 左侧一级 + 顶部二级
  | 'top-hybrid-header-first';   // 顶部一级 + 左侧二级

Theme.ThemePageAnimateMode

7 种页面切换动画:

type ThemePageAnimateMode =
  | 'none'
  | 'fade'
  | 'fade-slide'       // 默认
  | 'fade-bottom'
  | 'fade-scale'
  | 'zoom-fade'
  | 'zoom-out';

Theme.ThemePreset

type ThemePreset = ThemePresetMeta & Partial<ThemeSetting>;

interface ThemePresetMeta {
  name: string;
  desc: string;
  i18nkey: string;
  version: string;
  order: number;
}

默认配置参考

const defaultThemeSettings: Theme.ThemeSetting = {
  themeScheme: 'light',
  themeColor: '#6366F1',
  themeRadius: 6,
  themeTextSize: 14,
  grayscale: false,
  colourWeakness: false,
  recommendColor: false,
  isInfoFollowPrimary: false,
  otherColor: {
    info: '#0EA5E9',
    success: '#10B981',
    warning: '#F59E0B',
    error: '#F43F5E',
  },
  layout: { mode: 'vertical', scrollMode: 'content' },
  fixedHeaderAndTab: true,
  header: {
    height: 56,
    breadcrumb: { visible: true, showIcon: true },
    multilingual: { visible: true },
    globalSearch: { visible: true },
  },
  sider: {
    inverted: false,
    width: 220,
    collapsedWidth: 64,
    mixWidth: 90,
    mixCollapsedWidth: 64,
    mixChildMenuWidth: 200,
    autoSelectFirstMenu: false,
  },
  tab: { visible: true, cache: true, height: 44, mode: 'chrome', closeTabByMiddleClick: false },
  footer: { visible: true, fixed: false, height: 48, right: true },
  page: { animate: true, animateMode: 'fade-slide' },
  watermark: {
    visible: false,
    text: 'SkyrocAdmin',
    enableCustomText: true,
    enableUserName: false,
    enableTime: false,
    timeFormat: 'YYYY-MM-DD HH:mm',
    settings: { font: { fontSize: 16 }, height: 64, offset: [12, 60], rotate: -15, width: 344, zIndex: 9999 },
  },
  tokens: {
    light: {
      colors: {
        container: 'rgb(255, 255, 255)',
        layout: 'rgb(247, 250, 252)',
        inverted: 'rgb(0, 20, 40)',
        'base-text': 'rgb(31, 31, 31)',
      },
      boxShadow: {
        header: '0 1px 2px rgb(0, 21, 41, 0.08)',
        sider: '2px 0 8px 0 rgb(29, 35, 41, 0.05)',
        tab: '0 1px 2px rgb(0, 21, 41, 0.08)',
      },
    },
    dark: {
      colors: {
        container: 'rgb(28, 28, 28)',
        layout: 'rgb(18, 18, 18)',
        'base-text': 'rgb(224, 224, 224)',
      },
    },
  },
};

设计说明

单 atom 架构

整个 theme 状态存在一个 themeSettingsAtom 中,而非拆分为多个 atom。好处是:

  • 预设应用是原子操作,不会产生中间渲染状态
  • 持久化只需序列化一个对象
  • 重置只需写入一次

缺点是细粒度组件必须用 useMemo 选取自己关心的字段,但 Jotai 的 useAtomValue 本身是浅比较,加上 React 批量更新机制,实际多余渲染极少发生。

ThemeEffect 职责边界

所有 DOM 副作用和 storage 写入集中在 ThemeEffect 组件中,useTheme 本身不产生副作用。这样:

  • 组件树中挂载一次 ThemeEffect 即可
  • 副作用可观测、可调试,不隐藏在 hook 深处
  • 测试 useTheme 时无需 mock DOM 操作

storage 适配器在 setupTheme 调用时注入到包内部,ThemeEffect 直接使用,无需从外部 prop 传入。调用方不需要关心存储细节,也不会出现同一个 storage 对象被拆成两半传给两个入口的问题。

userName 通过 atom 传递

AntdProvider 接收 userName 后写入 themeUserNameAtom,而不是通过 React context 或 prop drilling 传递。所有调用 useTheme() 的组件都能自动获取完整的 watermarkContent,无需额外传参。

On this page