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' * import { setBrand, setBrandSecondary, applyStoredBrands } from '@/assets/theme'
* setBrand('#0f766e') // 改主色(自动重算变体) * setBrand('#0f766e') // 改主色(自动重算变体)
* setBrand('#1E3A8A') // 改回默认 * setBrand('#1E3A8A') // 改回默认
* setBrand('#1E3A8A', { persist: true }) // 写 localStorage * setBrand('#1E3A8A', { persist: true }) // 写 localStorage
* applyStoredBrand() // 应用持久化的颜色 * setBrandSecondary('#009963', { persist: true }) // 改次色 (绿)
* applyStoredBrands() // 启动时调用, 同时应用两个
* *
* 派生色算法(HSL 空间调整,比简单 hex 加减更稳): * 派生色算法(HSL 空间调整,比简单 hex 加减更稳):
* deep: lightness -= 12 * deep: lightness -= 12
@@ -15,9 +16,13 @@
* lighter: lightness += 35 * lighter: lightness += 35
* text: lightness += 22 * text: lightness += 22
* mix: 50% alpha + white (用于 focus border) * 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} */ /** hex -> {r,g,b} */
function hexToRgb(hex) { function hexToRgb(hex) {
@@ -73,38 +78,70 @@ function hslToHex({ h, s, l }) {
function clampL(l) { return Math.max(8, Math.min(92, 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) { export function derivePalette(base, prefix = 'primary') {
const hsl = rgbToHsl(hexToRgb(primary)) const hsl = rgbToHsl(hexToRgb(base))
const p = prefix
return { return {
'--brand-primary': primary, [`--brand-${p}`]: base,
'--brand-primary-deep': hslToHex({ ...hsl, l: clampL(hsl.l - 12) }), [`--brand-${p}-deep`]: hslToHex({ ...hsl, l: clampL(hsl.l - 12) }),
'--brand-primary-darker': hslToHex({ ...hsl, l: clampL(hsl.l - 22) }), [`--brand-${p}-darker`]: hslToHex({ ...hsl, l: clampL(hsl.l - 22) }),
'--brand-primary-light': hslToHex({ ...hsl, l: clampL(hsl.l + 12) }), [`--brand-${p}-light`]: hslToHex({ ...hsl, l: clampL(hsl.l + 12) }),
'--brand-primary-lighter': hslToHex({ ...hsl, l: clampL(hsl.l + 35) }), [`--brand-${p}-lighter`]: hslToHex({ ...hsl, l: clampL(hsl.l + 35) }),
'--brand-primary-text': hslToHex({ ...hsl, l: clampL(hsl.l + 22) }), [`--brand-${p}-text`]: hslToHex({ ...hsl, l: clampL(hsl.l + 22) }),
'--brand-primary-mix': hslToHex({ ...hsl, l: clampL(hsl.l + 45) }) [`--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 {string} primary 主色 hex
* @param {object} [opts] { persist: bool } 是否写 localStorage * @param {object} [opts] { persist: bool } 是否写 localStorage
*/ */
export function setBrand(primary, opts = {}) { export function setBrand(primary, opts = {}) {
const palette = derivePalette(primary) const palette = derivePalette(primary, 'primary')
const root = document.documentElement const root = document.documentElement
for (const [k, v] of Object.entries(palette)) root.style.setProperty(k, v) 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() { export function applyStoredBrand() {
const stored = localStorage.getItem(KEY) const stored = localStorage.getItem(KEY_PRIMARY)
if (stored) setBrand(stored) if (stored) setBrand(stored)
} }
@@ -114,3 +151,10 @@ export function applyStoredBrand() {
export function getBrand() { export function getBrand() {
return getComputedStyle(document.documentElement).getPropertyValue('--brand-primary').trim() 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 品牌主题色(Brand Theme
------------------------------------------------------------ ------------------------------------------------------------
所有色值都是 HSL/HEX 函数动态计算的派生色。 所有色值都是 HSL/HEX 函数动态计算的派生色。
改 :root 里的 --brand-primary 即可全局换色。 改 :root 里的 --brand-primary / --brand-secondary 即可全局换色。
派生色算法(hex → HSL → 调整亮度/饱和 → 转回 hex): 派生色算法(hex → HSL → 调整亮度/饱和 → 转回 hex):
--brand-primary 主色 --brand-primary 主色
@@ -13,11 +13,17 @@
--brand-primary-text 浅 35% (链接默认) --brand-primary-text 浅 35% (链接默认)
--brand-primary-mix 50% 透明 + 白 (输入框 focus border --brand-primary-mix 50% 透明 + 白 (输入框 focus border
Element Plus 同步:main.js 在启动时读取 --brand-primary 次色(绿色 #009963)— 走同一套派生算法, 命名 -secondary
并通过 ElMessage / ElButton 的 --el-color-primary 联动。 语义上对应 Element Plus success (绿=成功/已批准/已完成), 自动
绑定到 --el-color-success*, 这样所有 <el-button type="success"> /
el-tag type="success" 等都跟随品牌绿。
Element Plus 同步:main.js 在启动时读取两个 CSS 变量
并通过 ElMessage / ElButton 的 --el-color-* 联动。
============================================================= */ ============================================================= */
:root { :root {
/* 主色 — 青蓝 */
--brand-primary: #007A95; --brand-primary: #007A95;
--brand-primary-deep: #004858; --brand-primary-deep: #004858;
--brand-primary-darker: #002129; --brand-primary-darker: #002129;
@@ -25,15 +31,34 @@
--brand-primary-lighter: #49deff; --brand-primary-lighter: #49deff;
--brand-primary-text: #06d2ff; --brand-primary-text: #06d2ff;
--brand-primary-mix: #7ce7ff; --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 内置变量。 /* Element Plus 联动:把品牌色同步给 Element Plus 内置变量。
这样所有 <el-button type="primary"> 等都跟主题色。 */ 这样所有 <el-button type="primary"> / type="success"> 等都跟主题色。 */
:root { :root {
/* 主色 → primary */
--el-color-primary: var(--brand-primary); --el-color-primary: var(--brand-primary);
--el-color-primary-light-3: var(--brand-primary-light); --el-color-primary-light-3: var(--brand-primary-light);
--el-color-primary-light-5: var(--brand-primary-lighter); --el-color-primary-light-5: var(--brand-primary-lighter);
--el-color-primary-light-7: 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-light-9: var(--brand-primary-lighter);
--el-color-primary-dark-2: var(--brand-primary-deep); --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 router from './router'
import './assets/main.scss' import './assets/main.scss'
import './assets/theme.scss' import './assets/theme.scss'
import { applyStoredBrand } from './assets/theme' import { applyStoredBrands } from './assets/theme'
const app = createApp(App) const app = createApp(App)
for (const [k, v] of Object.entries(ElIcons)) app.component(k, v) for (const [k, v] of Object.entries(ElIcons)) app.component(k, v)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus, { locale: zhCn }) app.use(ElementPlus, { locale: zhCn })
applyStoredBrand() applyStoredBrands()
app.mount('#app') app.mount('#app')