208 lines
7.6 KiB
JavaScript
208 lines
7.6 KiB
JavaScript
import { colorToRgbChannels } from './color'
|
||
import { cloneThemeSettings, themeSettings, THEME_SETTINGS_VERSION } from './settings'
|
||
import { KRA_MAIN_VERSION, PRESET_FALLBACK_MIN_VERSION } from './version'
|
||
import { themeVars } from './vars'
|
||
import { defu, isObject, liteClone } from 'arcdash'
|
||
|
||
export const THEME_SETTINGS_STORAGE_KEY = 'kra-theme-settings'
|
||
|
||
// 颜色统一小写:项目内部所有颜色字符串以小写为单一形态,避免默认值「#2264f2」
|
||
// 与预设「#2264f2」因大小写不等而出现选中态丢失。任何外部数据进入运行态前都过一遍。
|
||
export const normalizeColor = (value) =>
|
||
typeof value === 'string' ? value.toLowerCase() : value
|
||
|
||
// 把一份 settings 里的主题色字段统一小写(就地改写后返回同一对象)。
|
||
const normalizeThemeColors = (settings) => {
|
||
settings.themeColor = normalizeColor(settings.themeColor)
|
||
if (isObject(settings.otherColor)) {
|
||
Object.keys(settings.otherColor).forEach((key) => {
|
||
settings.otherColor[key] = normalizeColor(settings.otherColor[key])
|
||
})
|
||
}
|
||
return settings
|
||
}
|
||
|
||
// 把 source 深合并进 target(就地,供 themeStore 直接改写响应式 settings)。
|
||
// target / source 都来自完整 schema(默认值或 normalizeThemeSettings 输出),
|
||
// 未知字段的过滤已在 normalize 阶段由 pickKnownKeys 完成,这里无需再做白名单。
|
||
export const mergeThemeSettings = (target, source) => {
|
||
Object.entries(source || {}).forEach(([key, value]) => {
|
||
if (isObject(value) && isObject(target[key])) {
|
||
mergeThemeSettings(target[key], value)
|
||
return
|
||
}
|
||
target[key] = value
|
||
})
|
||
}
|
||
|
||
// 递归按默认 schema 过滤未知字段:schema 中不存在的键一律丢弃,
|
||
// 避免外部数据污染运行态、拖累 adapter 扩展。
|
||
const pickKnownKeys = (source, schema) => {
|
||
if (!isObject(source) || !isObject(schema)) return source
|
||
|
||
const result = {}
|
||
Object.keys(schema).forEach((key) => {
|
||
if (key in source) result[key] = pickKnownKeys(source[key], schema[key])
|
||
})
|
||
return result
|
||
}
|
||
|
||
const isThemeSettingsLike = (source) =>
|
||
isObject(source) && Boolean(
|
||
source.themeScheme ||
|
||
source.themeColor ||
|
||
source.otherColor ||
|
||
source.layout ||
|
||
source.page ||
|
||
source.header ||
|
||
source.tab ||
|
||
source.menu ||
|
||
source.card ||
|
||
source.watermark ||
|
||
source.tokens
|
||
)
|
||
|
||
/**
|
||
* 把任意外部主题数据(后端 originSetting、导入预设的内层 theme、本地缓存)规整成
|
||
* 一份完整、可信的 settings。兼容策略(大版本,不迁移旧值,只做防御式回落):
|
||
* - `{ version, settings }` 信封:版本不等于当前版本 → 回落默认值;否则取 settings 继续。
|
||
* - 旧的扁平主题数据(不含新结构字段)→ isThemeSettingsLike 为 false → 回落默认值。
|
||
* - 合法的新结构 → 先按 schema 过滤未知键(pickKnownKeys),再用 defu 覆盖到默认值之上
|
||
* (source 优先,缺失 / undefined 自动回落默认;未知键已被剔除,不会进入运行态)。
|
||
*/
|
||
// 值级迁移:老版本把「深色」当成一种菜单风格(menu.theme==='dark')。新版本里深色是与风格
|
||
// 正交的独立开关(menu.darkSider),这里把老值翻译过去,保住老用户外观。
|
||
const migrateLegacyDarkMenu = (settings) => {
|
||
if (settings?.menu?.theme === 'dark') {
|
||
settings.menu.theme = 'design'
|
||
settings.menu.darkSider = true
|
||
}
|
||
return settings
|
||
}
|
||
|
||
export const normalizeThemeSettings = (input) => {
|
||
// 带 version 字段的信封:只接受当前大版本,否则视为不兼容数据,回落默认。
|
||
if (isObject(input) && 'version' in input && input.version !== THEME_SETTINGS_VERSION) {
|
||
return cloneThemeSettings()
|
||
}
|
||
|
||
const source = input?.theme || input?.settings || input
|
||
|
||
if (!isThemeSettingsLike(source)) return cloneThemeSettings()
|
||
|
||
return migrateLegacyDarkMenu(
|
||
normalizeThemeColors(defu(pickKnownKeys(source, themeSettings), cloneThemeSettings()))
|
||
)
|
||
}
|
||
|
||
export const normalizeThemePreset = (preset) => {
|
||
if (!preset) return null
|
||
|
||
const source = preset.theme || preset.settings
|
||
if (!source) return null
|
||
|
||
// 丢弃旧预设里的 ElConf/elConf 与原始 settings 键,统一输出规范化的内层 theme。
|
||
const rest = { ...preset }
|
||
delete rest.theme
|
||
delete rest.settings
|
||
delete rest.ElConf
|
||
delete rest.elConf
|
||
|
||
return {
|
||
...rest,
|
||
// version 是预设自身的发布迭代号,沿用作者声明的值,缺省记为 1。
|
||
version: preset.version ?? 1,
|
||
// minMainVersion 才是与 KRA 主题生态的兼容判据;旧预设未声明时兜底为始终兼容。
|
||
minMainVersion: preset.minMainVersion ?? PRESET_FALLBACK_MIN_VERSION,
|
||
theme: normalizeThemeSettings(source)
|
||
}
|
||
}
|
||
|
||
export const buildThemePreset = (settings, name) => ({
|
||
// 用户导出当前配置 = 一个全新预设,发布号从 1 起;
|
||
// minMainVersion 记为导出时的运行版本,即「能用该预设的最低 KRA 版本」。
|
||
version: 1,
|
||
minMainVersion: KRA_MAIN_VERSION,
|
||
name,
|
||
builtin: false,
|
||
theme: liteClone(settings)
|
||
})
|
||
|
||
/** 主题持久化信封:本地缓存与后端 originSetting 共用同一形态,便于版本校验。 */
|
||
export const buildThemeEnvelope = (settings) => ({
|
||
version: THEME_SETTINGS_VERSION,
|
||
settings: liteClone(settings)
|
||
})
|
||
|
||
export const readCachedThemeSettings = () => {
|
||
try {
|
||
const raw = localStorage.getItem(THEME_SETTINGS_STORAGE_KEY)
|
||
if (!raw) return null
|
||
|
||
const parsed = JSON.parse(raw)
|
||
// 严格按版本信封读取:版本不匹配(含旧的无版本缓存)一律视为失效,交由调用方回落默认。
|
||
if (parsed?.version !== THEME_SETTINGS_VERSION) return null
|
||
|
||
return normalizeThemeSettings(parsed.settings)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
export const cacheThemeSettings = (settings) => {
|
||
localStorage.setItem(
|
||
THEME_SETTINGS_STORAGE_KEY,
|
||
JSON.stringify(buildThemeEnvelope(settings))
|
||
)
|
||
}
|
||
|
||
export const clearCachedThemeSettings = () => {
|
||
localStorage.removeItem(THEME_SETTINGS_STORAGE_KEY)
|
||
}
|
||
|
||
const getCssVarByTokens = (tokens) => {
|
||
const styles = []
|
||
|
||
Object.entries(themeVars).forEach(([group, vars]) => {
|
||
Object.entries(vars).forEach(([tokenKey, tokenValue]) => {
|
||
let cssVarKey = tokenValue.replace('var(', '').replace(')', '')
|
||
let cssValue = tokens[group]?.[tokenKey]
|
||
|
||
if (cssValue === undefined) return
|
||
|
||
if (group === 'colors') {
|
||
cssVarKey = cssVarKey.replace('rgb(', '').replace(')', '')
|
||
cssValue = colorToRgbChannels(cssValue)
|
||
}
|
||
|
||
styles.push(`${cssVarKey}: ${cssValue}`)
|
||
})
|
||
})
|
||
|
||
return styles.join(';')
|
||
}
|
||
|
||
export const addThemeVarsToGlobal = (tokens, darkTokens) => {
|
||
const css = `:root { ${getCssVarByTokens(tokens)} }`
|
||
const darkCss = `html.dark { ${getCssVarByTokens(darkTokens)} }`
|
||
// 作用域暗色:浅色主题下给侧栏容器加 .kra-sider-dark,即让其子树的语义 token 翻成暗色。
|
||
// 与 html.dark 共用同一套 darkTokens,保证「浅色 + 深色侧栏」与「全局暗色侧栏」观感一致。
|
||
const scopedDarkCss = `.kra-sider-dark { ${getCssVarByTokens(darkTokens)} }`
|
||
const styleId = 'theme-vars'
|
||
const style = document.querySelector(`#${styleId}`) || document.createElement('style')
|
||
|
||
style.id = styleId
|
||
style.textContent = css + darkCss + scopedDarkCss
|
||
document.head.appendChild(style)
|
||
}
|
||
|
||
export const toggleCssDarkMode = (isDark) => {
|
||
document.documentElement.classList.toggle('dark', isDark)
|
||
document.documentElement.classList.toggle('light', !isDark)
|
||
}
|
||
|
||
export const toggleAuxiliaryColorModes = (grayscale, colourWeakness) => {
|
||
document.documentElement.classList.toggle('html-grey', grayscale)
|
||
document.documentElement.classList.toggle('html-weakenss', colourWeakness)
|
||
}
|