kra-new/web/src/theme/shared.js

208 lines
7.6 KiB
JavaScript
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.

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)
}