/**
|
* 主题色运行时计算与应用
|
* ---------------------------------------------------------------
|
* 背景:Element UI 2.x 的主题色是 SCSS 编译期常量,运行时无法改动;
|
* 原有 ThemePicker 依赖 unpkg CDN 拉取 theme-chalk 再字符串替换,
|
* 内网环境拉不到 CSS,导致"改主题没反应"。
|
* 做法:由任意主色推出一套色阶(hover / active / light-3~95),
|
* 写入 :root 的 CSS 变量;theme-runtime.scss 用 var() 消费这些变量,
|
* 从而实现"一次编译、运行时换肤、页面不刷新"。
|
*/
|
|
/** 默认主色:OPO_Logo.png 圆环青 */
|
export const DEFAULT_PRIMARY = '#0E768F'
|
|
/** localStorage 键(与布局设置分开,避免互相覆盖) */
|
const STORAGE_KEY = 'opo-theme'
|
|
/** 预设皮肤 */
|
export const PRESETS = [
|
{ name: '品牌青', color: '#0E768F' },
|
{ name: '玫红', color: '#CD1D4E' },
|
{ name: 'Element 蓝', color: '#409EFF' },
|
{ name: '叶芽绿', color: '#1B8E70' },
|
{ name: '紫', color: '#7B61FF' },
|
{ name: '橙', color: '#E6843C' }
|
]
|
|
const WHITE = [255, 255, 255]
|
const BLACK = [0, 0, 0]
|
|
function clamp(v) {
|
return Math.max(0, Math.min(255, Math.round(v)))
|
}
|
|
/** #abc / #aabbcc → [r,g,b];非法返回 null */
|
export function hexToRgb(hex) {
|
if (typeof hex !== 'string') return null
|
let h = hex.trim().replace(/^#/, '')
|
if (h.length === 3) {
|
h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]
|
}
|
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null
|
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]
|
}
|
|
/** [r,g,b] → #RRGGBB */
|
export function rgbToHex(rgb) {
|
return (
|
'#' +
|
rgb
|
.map(function (v) {
|
const s = clamp(v).toString(16)
|
return s.length === 1 ? '0' + s : s
|
})
|
.join('')
|
.toUpperCase()
|
)
|
}
|
|
/**
|
* 混色:weight 为目标色占比(0~1)
|
* mix([14,118,143], WHITE, 0.3) ≈ 30% 白 + 70% 主色
|
*/
|
export function mix(rgb, target, weight) {
|
const w = Math.max(0, Math.min(1, weight))
|
return [
|
clamp(rgb[0] * (1 - w) + target[0] * w),
|
clamp(rgb[1] * (1 - w) + target[1] * w),
|
clamp(rgb[2] * (1 - w) + target[2] * w)
|
]
|
}
|
|
function channelLum(v) {
|
const c = v / 255
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
}
|
|
/** 相对亮度(WCAG) */
|
export function luminance(rgb) {
|
return 0.2126 * channelLum(rgb[0]) + 0.7152 * channelLum(rgb[1]) + 0.0722 * channelLum(rgb[2])
|
}
|
|
/** 与白色的对比度,用于判断白字是否可读 */
|
export function contrastWithWhite(rgb) {
|
return 1.05 / (luminance(rgb) + 0.05)
|
}
|
|
/**
|
* 由主色推全套色阶
|
* 深色主色(亮度低)hover 用"加深",浅色主色 hover 用"变浅",
|
* 保证 hover 态的白字对比度不会掉下去。
|
*/
|
export function buildScale(primary) {
|
const base = hexToRgb(primary) || hexToRgb(DEFAULT_PRIMARY)
|
const isDark = luminance(base) < 0.4
|
|
const hover = isDark ? mix(base, BLACK, 0.1) : mix(base, WHITE, 0.2)
|
const active = isDark ? mix(base, BLACK, 0.2) : mix(base, WHITE, 0.32)
|
|
return {
|
primary: rgbToHex(base),
|
hover: rgbToHex(hover),
|
active: rgbToHex(active),
|
light3: rgbToHex(mix(base, WHITE, 0.3)),
|
light5: rgbToHex(mix(base, WHITE, 0.5)),
|
light7: rgbToHex(mix(base, WHITE, 0.7)),
|
light8: rgbToHex(mix(base, WHITE, 0.8)),
|
light9: rgbToHex(mix(base, WHITE, 0.9)),
|
light95: rgbToHex(mix(base, WHITE, 0.95)),
|
dark2: rgbToHex(mix(base, BLACK, 0.2)),
|
isDark: isDark,
|
contrastWithWhite: Number(contrastWithWhite(base).toFixed(2))
|
}
|
}
|
|
/** 把色阶写入 :root 的 CSS 变量(theme-runtime.scss 消费) */
|
export function applyTheme(primary) {
|
const scale = buildScale(primary || DEFAULT_PRIMARY)
|
if (typeof document === 'undefined') return scale
|
|
const root = document.documentElement
|
const map = {
|
'--opo-primary': scale.primary,
|
'--opo-primary-hover': scale.hover,
|
'--opo-primary-active': scale.active,
|
'--opo-primary-light-3': scale.light3,
|
'--opo-primary-light-5': scale.light5,
|
'--opo-primary-light-7': scale.light7,
|
'--opo-primary-light-8': scale.light8,
|
'--opo-primary-light-9': scale.light9,
|
'--opo-primary-light-95': scale.light95,
|
'--opo-primary-dark-2': scale.dark2
|
}
|
Object.keys(map).forEach(function (k) {
|
root.style.setProperty(k, map[k])
|
})
|
// 供个别需要知道"当前是否深色主色"的场景使用
|
root.setAttribute('data-opo-dark-primary', scale.isDark ? '1' : '0')
|
return scale
|
}
|
|
/** 读取上次选择的主色 */
|
export function getStoredTheme() {
|
try {
|
return localStorage.getItem(STORAGE_KEY)
|
} catch (e) {
|
return null
|
}
|
}
|
|
/** 记住选择 */
|
export function setStoredTheme(color) {
|
try {
|
localStorage.setItem(STORAGE_KEY, color)
|
} catch (e) {
|
/* 忽略隐私模式等异常 */
|
}
|
}
|
|
export default {
|
DEFAULT_PRIMARY: DEFAULT_PRIMARY,
|
PRESETS: PRESETS,
|
applyTheme: applyTheme,
|
buildScale: buildScale,
|
getStoredTheme: getStoredTheme,
|
setStoredTheme: setStoredTheme,
|
hexToRgb: hexToRgb,
|
rgbToHex: rgbToHex,
|
mix: mix,
|
luminance: luminance,
|
contrastWithWhite: contrastWithWhite
|
}
|