feat(theme): 加 --brand-secondary 翠绿 #009963 (双品牌色系)

- theme.scss 加 7 个 --brand-secondary-* 派生色 (HSL 算法)
- 绑定 Element Plus --el-color-success* (绿=成功语义)
- theme.js derivePalette 重构成通用函数 (base, prefix), 加 setBrandSecondary / applyStoredBrands
- main.js 启动改调 applyStoredBrands, 主色+次色一起从 localStorage 恢复

主色 primary (青蓝 #007A95) / 次色 secondary (翠绿 #009963)
改 theme.scss :root 两个变量即可全局换色.
This commit is contained in:
郭庆泰
2026-08-22 20:46:28 +08:00
parent e5fb015cd8
commit 884eadcd8e
3 changed files with 94 additions and 25 deletions
+63 -19
View File
@@ -2,11 +2,12 @@
* 品牌主题色切换工具
* ----------------------------------------
* 用法:
* import { setBrand } from '@/assets/theme'
* setBrand('#0f766e') // 改主色(自动重算变体)
* import { setBrand, setBrandSecondary, applyStoredBrands } from '@/assets/theme'
* setBrand('#0f766e') // 改主色(自动重算变体)
* setBrand('#1E3A8A') // 改回默认
* setBrand('#1E3A8A', { persist: true }) // 写 localStorage
* applyStoredBrand() // 应用持久化的颜色
* setBrandSecondary('#009963', { persist: true }) // 改次色 (绿)
* applyStoredBrands() // 启动时调用, 同时应用两个
*
* 派生色算法(HSL 空间调整,比简单 hex 加减更稳):
* deep: lightness -= 12
@@ -15,9 +16,13 @@
* lighter: lightness += 35
* text: lightness += 22
* mix: 50% alpha + white (用于 focus border)
*
* 双色系: 主色 (primary, 青蓝) + 次色 (secondary, 翠绿).
* 主色 → Element Plus --el-color-primary*; 次色 → --el-color-success* (绿=成功语义).
*/
const KEY = 'brand-primary'
const KEY_PRIMARY = 'brand-primary'
const KEY_SECONDARY = 'brand-secondary'
/** hex -> {r,g,b} */
function hexToRgb(hex) {
@@ -73,38 +78,70 @@ function hslToHex({ h, s, l }) {
function clampL(l) { return Math.max(8, Math.min(92, l)) }
/**
* 根据主色算出 7 个派生色 hex
* 通用派生算法:输入主色 hex, 输出 7 个 --brand-<prefix>-* 变量
* @param {string} base 主色 hex
* @param {string} [prefix='primary'] 变量名前缀 (primary / secondary)
*/
export function derivePalette(primary) {
const hsl = rgbToHsl(hexToRgb(primary))
export function derivePalette(base, prefix = 'primary') {
const hsl = rgbToHsl(hexToRgb(base))
const p = prefix
return {
'--brand-primary': primary,
'--brand-primary-deep': hslToHex({ ...hsl, l: clampL(hsl.l - 12) }),
'--brand-primary-darker': hslToHex({ ...hsl, l: clampL(hsl.l - 22) }),
'--brand-primary-light': hslToHex({ ...hsl, l: clampL(hsl.l + 12) }),
'--brand-primary-lighter': hslToHex({ ...hsl, l: clampL(hsl.l + 35) }),
'--brand-primary-text': hslToHex({ ...hsl, l: clampL(hsl.l + 22) }),
'--brand-primary-mix': hslToHex({ ...hsl, l: clampL(hsl.l + 45) })
[`--brand-${p}`]: base,
[`--brand-${p}-deep`]: hslToHex({ ...hsl, l: clampL(hsl.l - 12) }),
[`--brand-${p}-darker`]: hslToHex({ ...hsl, l: clampL(hsl.l - 22) }),
[`--brand-${p}-light`]: hslToHex({ ...hsl, l: clampL(hsl.l + 12) }),
[`--brand-${p}-lighter`]: hslToHex({ ...hsl, l: clampL(hsl.l + 35) }),
[`--brand-${p}-text`]: hslToHex({ ...hsl, l: clampL(hsl.l + 22) }),
[`--brand-${p}-mix`]: hslToHex({ ...hsl, l: clampL(hsl.l + 45) })
}
}
/**
* 设置 :root 上 7 个 CSS 变量
* 次色派生 (兼容旧调用, 等价 derivePalette(base, 'secondary'))
*/
export function deriveSecondaryPalette(secondary) {
return derivePalette(secondary, 'secondary')
}
/**
* 设置 :root 上主色 7 个 CSS 变量
* @param {string} primary 主色 hex
* @param {object} [opts] { persist: bool } 是否写 localStorage
*/
export function setBrand(primary, opts = {}) {
const palette = derivePalette(primary)
const palette = derivePalette(primary, 'primary')
const root = document.documentElement
for (const [k, v] of Object.entries(palette)) root.style.setProperty(k, v)
if (opts.persist) localStorage.setItem(KEY, primary)
if (opts.persist) localStorage.setItem(KEY_PRIMARY, primary)
}
/**
* 应用持久化在 localStorage 的颜色(无则保持现状)
* 设置 :root 上次色 7 个 CSS 变量
* @param {string} secondary 次色 hex
* @param {object} [opts] { persist: bool }
*/
export function setBrandSecondary(secondary, opts = {}) {
const palette = derivePalette(secondary, 'secondary')
const root = document.documentElement
for (const [k, v] of Object.entries(palette)) root.style.setProperty(k, v)
if (opts.persist) localStorage.setItem(KEY_SECONDARY, secondary)
}
/**
* 一次性应用两个持久化颜色 (无则保持现状)
*/
export function applyStoredBrands() {
const p = localStorage.getItem(KEY_PRIMARY)
if (p) setBrand(p)
const s = localStorage.getItem(KEY_SECONDARY)
if (s) setBrandSecondary(s)
}
/**
* 应用持久化的主色 (无则保持现状)
*/
export function applyStoredBrand() {
const stored = localStorage.getItem(KEY)
const stored = localStorage.getItem(KEY_PRIMARY)
if (stored) setBrand(stored)
}
@@ -113,4 +150,11 @@ export function applyStoredBrand() {
*/
export function getBrand() {
return getComputedStyle(document.documentElement).getPropertyValue('--brand-primary').trim()
}
/**
* 当前 :root 的次色(只读)
*/
export function getBrandSecondary() {
return getComputedStyle(document.documentElement).getPropertyValue('--brand-secondary').trim()
}
+29 -4
View File
@@ -2,7 +2,7 @@
品牌主题色(Brand Theme
------------------------------------------------------------
所有色值都是 HSL/HEX 函数动态计算的派生色。
改 :root 里的 --brand-primary 即可全局换色。
改 :root 里的 --brand-primary / --brand-secondary 即可全局换色。
派生色算法(hex → HSL → 调整亮度/饱和 → 转回 hex):
--brand-primary 主色
@@ -13,11 +13,17 @@
--brand-primary-text 浅 35% (链接默认)
--brand-primary-mix 50% 透明 + 白 (输入框 focus border
Element Plus 同步:main.js 在启动时读取 --brand-primary
并通过 ElMessage / ElButton 的 --el-color-primary 联动。
次色(绿色 #009963)— 走同一套派生算法, 命名 -secondary
语义上对应 Element Plus success (绿=成功/已批准/已完成), 自动
绑定到 --el-color-success*, 这样所有 <el-button type="success"> /
el-tag type="success" 等都跟随品牌绿。
Element Plus 同步:main.js 在启动时读取两个 CSS 变量
并通过 ElMessage / ElButton 的 --el-color-* 联动。
============================================================= */
:root {
/* 主色 — 青蓝 */
--brand-primary: #007A95;
--brand-primary-deep: #004858;
--brand-primary-darker: #002129;
@@ -25,15 +31,34 @@
--brand-primary-lighter: #49deff;
--brand-primary-text: #06d2ff;
--brand-primary-mix: #7ce7ff;
/* 次色 — 翠绿 */
--brand-secondary: #009963;
--brand-secondary-deep: #005c3b;
--brand-secondary-darker: #00291a;
--brand-secondary-light: #00d68b;
--brand-secondary-lighter: #4dffc0;
--brand-secondary-text: #0affa9;
--brand-secondary-mix: #80ffd2;
}
/* Element Plus 联动:把品牌色同步给 Element Plus 内置变量。
这样所有 <el-button type="primary"> 等都跟主题色。 */
这样所有 <el-button type="primary"> / type="success"> 等都跟主题色。 */
:root {
/* 主色 → primary */
--el-color-primary: var(--brand-primary);
--el-color-primary-light-3: var(--brand-primary-light);
--el-color-primary-light-5: var(--brand-primary-lighter);
--el-color-primary-light-7: var(--brand-primary-lighter);
--el-color-primary-light-9: var(--brand-primary-lighter);
--el-color-primary-dark-2: var(--brand-primary-deep);
/* 次色 → success (绿色语义: 已批准 / 已完成) */
--el-color-success: var(--brand-secondary);
--el-color-success-light-3: var(--brand-secondary-light);
--el-color-success-light-5: var(--brand-secondary-lighter);
--el-color-success-light-7: var(--brand-secondary-lighter);
--el-color-success-light-8: var(--brand-secondary-lighter);
--el-color-success-light-9: var(--brand-secondary-lighter);
--el-color-success-dark-2: var(--brand-secondary-deep);
}
+2 -2
View File
@@ -8,12 +8,12 @@ import App from './App.vue'
import router from './router'
import './assets/main.scss'
import './assets/theme.scss'
import { applyStoredBrand } from './assets/theme'
import { applyStoredBrands } from './assets/theme'
const app = createApp(App)
for (const [k, v] of Object.entries(ElIcons)) app.component(k, v)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
applyStoredBrand()
applyStoredBrands()
app.mount('#app')