/** * 主题色运行时计算与应用 * --------------------------------------------------------------- * 背景: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 }