/** * 主题色工具:根据取色器选中的主色,计算 hover/light 并写入 :root CSS 变量 * 用于导航栏、侧边菜单、按钮等公共颜色统一切换 */ const STORAGE_KEY = 'app-theme-color'; const THEME_MODE_KEY = 'app-theme-mode'; /** 旧默认色:切到新主色时自动迁移,避免 localStorage 卡住旧主题 */ const LEGACY_THEME_COLORS = new Set([ '#132c44', '#132C44', '#2d8c8c', '#2D8C8C', '#007979', '#1d7281', '#1D7281', '#1e7fa0', '#1E7FA0', ]); export const DEFAULT_THEME_COLOR = '#054f74'; export type ThemeMode = 'light' | 'dark'; export function getStoredThemeColor(): string | null { try { const stored = localStorage.getItem(STORAGE_KEY); if (!stored) return null; if (LEGACY_THEME_COLORS.has(stored)) { localStorage.setItem(STORAGE_KEY, DEFAULT_THEME_COLOR); return DEFAULT_THEME_COLOR; } return stored; } catch { return null; } } export function setStoredThemeColor(hex: string): void { try { localStorage.setItem(STORAGE_KEY, hex); } catch { // ignore } } export function getStoredThemeMode(): ThemeMode | null { try { const stored = localStorage.getItem(THEME_MODE_KEY); if (stored === 'light' || stored === 'dark') return stored; return null; } catch { return null; } } export function setStoredThemeMode(mode: ThemeMode): void { try { localStorage.setItem(THEME_MODE_KEY, mode); } catch { // ignore } } /** 将 hex 转为 r,g,b (0-255) */ function hexToRgb(hex: string): { r: number; g: number; b: number } | null { const m = hex.replace(/^#/, '').match(/^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); if (!m) return null; return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16), }; } /** 将 r,g,b 转为 #rrggbb */ function rgbToHex(r: number, g: number, b: number): string { return ( '#' + [r, g, b] .map((x) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, '0')) .join('') ); } /** 在 hex 颜色基础上按比例变暗(用于 hover) */ function darkenHex(hex: string, ratio: number): string { const rgb = hexToRgb(hex); if (!rgb) return hex; return rgbToHex(rgb.r * (1 - ratio), rgb.g * (1 - ratio), rgb.b * (1 - ratio)); } /** 生成浅色背景(用于 menu hover 等) */ function lightenHex(hex: string, mixWhite = 0.92): string { const rgb = hexToRgb(hex); if (!rgb) return hex; const r = Math.round(rgb.r * (1 - mixWhite) + 255 * mixWhite); const g = Math.round(rgb.g * (1 - mixWhite) + 255 * mixWhite); const b = Math.round(rgb.b * (1 - mixWhite) + 255 * mixWhite); return rgbToHex(r, g, b); } /** * 将主题色应用到页面:设置 :root 上的 CSS 变量 * @param primaryHex 主色,如 #1d7281 */ export function applyThemeColor(primaryHex: string): void { const hex = primaryHex.startsWith('#') ? primaryHex : `#${primaryHex}`; const root = document.documentElement; const rgb = hexToRgb(hex); const isDark = document.body.getAttribute('arco-theme') === 'dark'; const primary = hex; const primaryHover = darkenHex(hex, 0.05); const primaryLight = lightenHex(hex, 0.92); root.style.setProperty('--app-primary-color', primary); root.style.setProperty('--app-primary-color-hover', primaryHover); root.style.setProperty('--app-primary-color-light', primaryLight); root.style.setProperty('--color-primary-light-1', primaryLight); // 顶栏:日间用主色;夜间用深色壳(由 applyThemeMode / 下方分支写入) if (isDark) { applyNavbarDarkTokens(); } else { root.style.setProperty('--app-navbar-bg', primary); root.style.setProperty('--app-navbar-item-active-bg', lightenHex(hex, 0.16)); root.style.setProperty('--app-sidebar-bg', primary); } // 同步 Arco Design primary 色阶,保证按钮/链接等组件跟主题一致 if (rgb) { const rgbStr = `${rgb.r}, ${rgb.g}, ${rgb.b}`; const hover = hexToRgb(primaryHover); const light = hexToRgb(lightenHex(hex, 0.15)); root.style.setProperty('--primary-6', rgbStr); root.style.setProperty('--primary-7', hover ? `${hover.r}, ${hover.g}, ${hover.b}` : rgbStr); root.style.setProperty('--primary-5', light ? `${light.r}, ${light.g}, ${light.b}` : rgbStr); root.style.setProperty('--arcoblue-6', rgbStr); } } /** 夜间顶栏 / 侧栏表面色(与内容区一致的深色壳,选中胶囊带主色) */ function applyNavbarDarkTokens(): void { const root = document.documentElement; root.style.setProperty('--app-navbar-bg', 'var(--color-bg-2)'); root.style.setProperty( '--app-navbar-item-active-bg', 'color-mix(in srgb, var(--app-primary-color) 34%, var(--color-bg-3))', ); root.style.setProperty('--app-sidebar-bg', 'var(--color-bg-2)'); } /** 将日间/夜间模式应用到 DOM */ export function applyThemeMode(mode: ThemeMode): void { const root = document.documentElement; if (mode === 'dark') { document.body.setAttribute('arco-theme', 'dark'); root.setAttribute('data-theme', 'dark'); // 同步写到 :root,避免仅依赖 body 变量时列表壳仍读到 global.less 的浅色默认值 root.style.setProperty('--app-page-bg', 'var(--color-bg-1)'); root.style.setProperty('--app-card-bg', 'var(--color-bg-2)'); root.style.setProperty('--app-card-border', 'var(--color-border-2)'); root.style.setProperty('--app-elevated-bg', 'var(--color-bg-3)'); applyNavbarDarkTokens(); } else { document.body.removeAttribute('arco-theme'); root.setAttribute('data-theme', 'light'); root.style.removeProperty('--app-page-bg'); root.style.removeProperty('--app-card-bg'); root.style.removeProperty('--app-card-border'); root.style.removeProperty('--app-elevated-bg'); // 恢复日间主色顶栏 const primary = getComputedStyle(root).getPropertyValue('--app-primary-color').trim() || DEFAULT_THEME_COLOR; root.style.setProperty('--app-navbar-bg', primary); root.style.setProperty('--app-navbar-item-active-bg', lightenHex(primary, 0.16)); root.style.setProperty('--app-sidebar-bg', primary); } }