diff --git a/ry-vue3/src/assets/theme.js b/ry-vue3/src/assets/theme.js index 3ae78a8..76dc08c 100644 --- a/ry-vue3/src/assets/theme.js +++ b/ry-vue3/src/assets/theme.js @@ -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--* 变量 + * @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() } \ No newline at end of file diff --git a/ry-vue3/src/assets/theme.scss b/ry-vue3/src/assets/theme.scss index ea63ea4..93e4590 100644 --- a/ry-vue3/src/assets/theme.scss +++ b/ry-vue3/src/assets/theme.scss @@ -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-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 内置变量。 - 这样所有 等都跟主题色。 */ + 这样所有 / 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); } \ No newline at end of file diff --git a/ry-vue3/src/main.js b/ry-vue3/src/main.js index 450bae1..ec9f385 100644 --- a/ry-vue3/src/main.js +++ b/ry-vue3/src/main.js @@ -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')