yxh
4 小时以前 c75027743ea7c3ecc2d0885f01d11d2ad78679a3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
 * 主题色运行时计算与应用
 * ---------------------------------------------------------------
 * 背景: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
}