Files
mai-bot/dashboard/src/lib/theme/storage.ts

206 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 主题配置的 localStorage 存储管理模块
* 统一处理主题相关的存储操作,包括加载、保存、导出、导入和迁移旧 key
*/
import type { UserThemeConfig } from './tokens'
/**
* 主题存储 key 定义
* 统一使用 'maibot-theme-*' 前缀,替代现有的 'ui-theme'、'maibot-ui-theme' 和 'accent-color'
*/
export const THEME_STORAGE_KEYS = {
MODE: 'maibot-theme-mode',
PRESET: 'maibot-theme-preset',
ACCENT: 'maibot-theme-accent',
OVERRIDES: 'maibot-theme-overrides',
CUSTOM_CSS: 'maibot-theme-custom-css',
} as const
/**
* 默认主题配置
*/
const DEFAULT_THEME_CONFIG: UserThemeConfig = {
selectedPreset: 'light',
accentColor: 'blue',
tokenOverrides: {},
customCSS: '',
}
/**
* 从 localStorage 加载完整主题配置
* 缺失值使用合理默认值
*
* @returns 加载的主题配置对象
*/
export function loadThemeConfig(): UserThemeConfig {
const preset = localStorage.getItem(THEME_STORAGE_KEYS.PRESET)
const accent = localStorage.getItem(THEME_STORAGE_KEYS.ACCENT)
const overridesStr = localStorage.getItem(THEME_STORAGE_KEYS.OVERRIDES)
const customCSS = localStorage.getItem(THEME_STORAGE_KEYS.CUSTOM_CSS)
// 解析 tokenOverrides JSON
let tokenOverrides = {}
if (overridesStr) {
try {
tokenOverrides = JSON.parse(overridesStr)
} catch {
// JSON 解析失败,使用空对象
tokenOverrides = {}
}
}
return {
selectedPreset: preset || DEFAULT_THEME_CONFIG.selectedPreset,
accentColor: accent || DEFAULT_THEME_CONFIG.accentColor,
tokenOverrides,
customCSS: customCSS || DEFAULT_THEME_CONFIG.customCSS,
}
}
/**
* 保存完整主题配置到 localStorage
*
* @param config - 要保存的主题配置
*/
export function saveThemeConfig(config: UserThemeConfig): void {
localStorage.setItem(THEME_STORAGE_KEYS.PRESET, config.selectedPreset)
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, config.accentColor)
localStorage.setItem(THEME_STORAGE_KEYS.OVERRIDES, JSON.stringify(config.tokenOverrides))
localStorage.setItem(THEME_STORAGE_KEYS.CUSTOM_CSS, config.customCSS)
}
/**
* 部分更新主题配置
* 先加载现有配置,合并部分更新,再保存
*
* @param partial - 部分主题配置更新
*/
export function saveThemePartial(partial: Partial<UserThemeConfig>): void {
const current = loadThemeConfig()
const updated: UserThemeConfig = {
...current,
...partial,
}
saveThemeConfig(updated)
}
/**
* 导出主题配置为美化格式的 JSON 字符串
*
* @returns 格式化的 JSON 字符串
*/
export function exportThemeJSON(): string {
const config = loadThemeConfig()
return JSON.stringify(config, null, 2)
}
/**
* 从 JSON 字符串导入主题配置
* 包含基础的格式和字段校验
*
* @param json - JSON 字符串
* @returns 导入结果,包含成功状态和错误列表
*/
export function importThemeJSON(
json: string,
): { success: boolean; errors: string[] } {
const errors: string[] = []
// JSON 格式校验
let config: unknown
try {
config = JSON.parse(json)
} catch (error) {
return {
success: false,
errors: [`Invalid JSON format: ${error instanceof Error ? error.message : 'Unknown error'}`],
}
}
// 基本对象类型校验
if (typeof config !== 'object' || config === null) {
return {
success: false,
errors: ['Configuration must be a JSON object'],
}
}
const configObj = config as Record<string, unknown>
// 必要字段存在性校验
if (typeof configObj.selectedPreset !== 'string') {
errors.push('selectedPreset must be a string')
}
if (typeof configObj.accentColor !== 'string') {
errors.push('accentColor must be a string')
}
if (typeof configObj.customCSS !== 'string') {
errors.push('customCSS must be a string')
}
if (configObj.tokenOverrides !== undefined && typeof configObj.tokenOverrides !== 'object') {
errors.push('tokenOverrides must be an object')
}
if (errors.length > 0) {
return { success: false, errors }
}
// 校验通过,保存配置
const validConfig: UserThemeConfig = {
selectedPreset: configObj.selectedPreset as string,
accentColor: configObj.accentColor as string,
tokenOverrides: (configObj.tokenOverrides as Partial<any>) || {},
customCSS: configObj.customCSS as string,
}
saveThemeConfig(validConfig)
return { success: true, errors: [] }
}
/**
* 重置主题配置为默认值
* 删除所有 THEME_STORAGE_KEYS 对应的 localStorage 项
*/
export function resetThemeToDefault(): void {
Object.values(THEME_STORAGE_KEYS).forEach((key) => {
localStorage.removeItem(key)
})
}
/**
* 迁移旧的 localStorage key 到新 key
* 处理:
* - 'ui-theme' 或 'maibot-ui-theme' → 'maibot-theme-mode'
* - 'accent-color' → 'maibot-theme-accent'
* 迁移完成后删除旧 key避免重复迁移
*/
export function migrateOldKeys(): void {
// 迁移主题模式
// 优先使用 'ui-theme'(因为 ThemeProvider 默认使用它)
const uiTheme = localStorage.getItem('ui-theme')
const maiTheme = localStorage.getItem('maibot-ui-theme')
const newMode = localStorage.getItem(THEME_STORAGE_KEYS.MODE)
if (!newMode) {
if (uiTheme) {
localStorage.setItem(THEME_STORAGE_KEYS.MODE, uiTheme)
} else if (maiTheme) {
localStorage.setItem(THEME_STORAGE_KEYS.MODE, maiTheme)
}
}
// 迁移强调色
const accentColor = localStorage.getItem('accent-color')
const newAccent = localStorage.getItem(THEME_STORAGE_KEYS.ACCENT)
if (accentColor && !newAccent) {
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, accentColor)
}
// 删除旧 key
localStorage.removeItem('ui-theme')
localStorage.removeItem('maibot-ui-theme')
localStorage.removeItem('accent-color')
}