@skyroc/web-admin-theme
Web 管理端主题管理包 — Jotai 状态、React 组件、预设系统、副作用收口与 Ant Design 集成的完整解决方案
概述
@skyroc/web-admin-theme 是面向应用层的主题管理包,构建于 @skyroc/adapter-antd-theme 之上,提供:
- 全局状态:单一 Jotai atom 作为 theme 唯一数据源,所有组件共享
useThemehook:统一读写主题的入口,封装所有派生计算和 mutationAntdProvider组件:自动桥接 theme 状态与 Ant DesignConfigProvider,整合水印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 antdPeer dependencies:antd >= 6.0.0,react >= 19.0.0,jotai >= 2.0.0
快速上手
1. 初始化(应用入口)
在 main.tsx 或 app.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;
}初始化逻辑:
- 开发环境:直接用
defaultThemeSettings初始化 atom,不读 storage - 生产环境:
- 从
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 的展开):
| 字段 | 类型 | 说明 |
|---|---|---|
settings | Theme.ThemeSetting | 完整配置对象 |
themeScheme | ThemeMode | 当前模式:'light' | 'dark' | 'auto' |
themeColor | string | 主题色 hex |
darkMode | boolean | 当前是否处于暗色(auto 时跟随系统) |
themeColors | ThemeColor | { primary, info, success, warning, error } |
grayscaleMode | boolean | 灰度模式 |
colourWeaknessMode | boolean | 色弱模式 |
watermarkContent | string | 计算后的水印文字 |
settingsJson | string | 配置序列化字符串(用于比较变化) |
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 | 重置为默认配置 |
updateThemeColors 在 settings.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 等基础 tokencomponents:Button、Menu、Collapse 的默认样式调整
ThemeEffect
const ThemeEffect: React.FC渲染为 null,无需任何 props。storage 适配器通过 setupTheme 注入,包内部统一使用,不向外暴露。管理以下副作用:
| 监听值 | 副作用 |
|---|---|
darkMode | toggleCssDarkMode(darkMode) → <html> 加/去 "dark" class;写 storage darkMode |
grayscaleMode / colourWeaknessMode | toggleAuxiliaryColorModes() → document.documentElement.style.filter |
themeColors | 写 storage themeColor(主色 hex) |
watermark.visible / enableTime | updateWatermarkTimer() → 暂停 / 恢复水印时间定时器 |
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 | 紧凑间距,小字号,适合信息密集型界面 |
shadcn | shadcn/ui 风格,中性主色,大圆角 |
每个预设是完整的 Theme.ThemePreset(ThemePresetMeta & Partial<ThemeSetting>),通过 setSettings(preset) 应用。
工具函数
mergeThemeSettings
深度合并两个 ThemeSetting,source 优先于 base:
function mergeThemeSettings(
source: Partial<Theme.ThemeSetting> | null | undefined,
base: Theme.ThemeSetting
): Theme.ThemeSettinggetThemeColors
从 settings 提取 ThemeColor(处理 isInfoFollowPrimary 逻辑):
function getThemeColors(settings: Theme.ThemeSetting): ThemeColorgetDefaultThemeSettings
返回默认配置的深拷贝:
function getDefaultThemeSettings(): Theme.ThemeSettingtoggleCssDarkMode
切换 <html> 元素的 "dark" class:
function toggleCssDarkMode(darkMode: boolean): voidisDarkModeClass
检查当前 <html> 是否包含 "dark" class:
function isDarkModeClass(): booleantoggleAuxiliaryColorModes
同步应用灰度和色弱 CSS 滤镜:
function toggleAuxiliaryColorModes(
grayscale: boolean,
colourWeakness: boolean
): void灰度:filter: grayscale(100%);色弱:filter: invert(80%);同时开启叠加。
clearAuxiliaryColorModes
清除所有辅助颜色滤镜:
function clearAuxiliaryColorModes(): voidAnt 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,无需额外传参。