front/src/utils/themeColor.ts
zzh b5edbb8447 feat: 页面背景预设与工作台增强,优化官网与登录体验
新增石米等淡色背景预设并设为默认,修复页面配置抽屉偶发不弹出问题,移除登录欢迎卡;同步 IDE 官网导航与背景样式,并扩展管理端工作台统计展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 10:59:10 +08:00

182 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 主题色工具:根据取色器选中的主色,计算 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);
}
}