chore: Optimize multi-theme switching

This commit is contained in:
vben
2024-06-23 19:17:31 +08:00
parent aa53353903
commit 6afed34437
55 changed files with 3534 additions and 772 deletions

View File

@@ -30,6 +30,7 @@
},
"dependencies": {
"@vben-core/cache": "workspace:*",
"@vben-core/colorful": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",

View File

@@ -18,7 +18,6 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin Pro',
semiDarkMenu: true,
showPreference: true,
themeMode: 'dark',
},
breadcrumb: {
enable: true,
@@ -67,7 +66,13 @@ const defaultPreferences: Preferences = {
showIcon: true,
},
theme: {
colorPrimary: 'hsl(211 91% 39%)',
builtinType: 'default',
colorDestructive: 'hsl(348 100% 61%)',
colorPrimary: 'hsl(245 82% 67%)',
colorSuccess: 'hsl(144 57% 58%)',
colorWarning: 'hsl(42 84% 61%)',
mode: 'dark',
radius: '0.5',
},
transition: {
enable: true,

View File

@@ -1,3 +1,5 @@
import type { BuiltinThemeType } from '@vben-core/typings';
import type { SupportedLanguagesType } from './types';
interface Language {
@@ -5,17 +7,17 @@ interface Language {
text: string;
}
export const COLOR_PRIMARY_RESETS = [
'hsl(211 91% 39%)',
'hsl(212 100% 45%)',
'hsl(181 84% 32%)',
'hsl(161 90% 43%)',
'hsl(231 98% 65%)',
'hsl(245 82% 67%)',
'hsl(347 77% 60%)',
];
interface BuiltinThemePreset {
color: string;
darkPrimaryColor?: string;
primaryColor?: string;
type: BuiltinThemeType;
}
export const SUPPORT_LANGUAGES: Language[] = [
/**
* Supported languages
*/
const SUPPORT_LANGUAGES: Language[] = [
{
key: 'zh-CN',
text: '简体中文',
@@ -25,3 +27,80 @@ export const SUPPORT_LANGUAGES: Language[] = [
text: 'English',
},
];
const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
{
color: 'hsl(245 82% 67%)',
type: 'default',
},
{
color: 'hsl(231 98% 65%)',
type: 'violet',
},
{
color: 'hsl(347 77% 60%)',
type: 'pink',
},
{
color: 'hsl(0 75% 42%)',
type: 'rose',
},
{
color: 'hsl(212 100% 45%)',
type: 'sky-blue',
},
{
color: 'hsl(211 91% 39%)',
type: 'deep-blue',
},
{
color: 'hsl(161 90% 43%)',
type: 'green',
},
{
color: 'hsl(181 84% 32%)',
type: 'deep-green',
},
{
color: 'hsl(18 89% 40%)',
type: 'orange',
},
{
color: 'hsl(42 84% 61%)',
type: 'yellow',
},
{
color: 'hsl(240 5% 26%)',
darkPrimaryColor: 'hsl(0 0 98%)',
primaryColor: 'hsl(240 5.9% 10%)',
type: 'zinc',
},
{
color: 'hsl(0 0% 25%)',
darkPrimaryColor: 'hsl(0 0 98%)',
primaryColor: 'hsl(240 5.9% 10%)',
type: 'neutral',
},
{
color: 'hsl(215 25% 27%)',
darkPrimaryColor: 'hsl(0 0 98%)',
primaryColor: 'hsl(240 5.9% 10%)',
type: 'slate',
},
{
color: 'hsl(217 19% 27%)',
darkPrimaryColor: 'hsl(0 0 98%)',
primaryColor: 'hsl(240 5.9% 10%)',
type: 'gray',
},
{
color: '',
type: 'custom',
},
];
export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
export { BUILT_IN_THEME_PRESETS, SUPPORT_LANGUAGES };
export type { BuiltinThemePreset };

View File

@@ -55,7 +55,6 @@ describe('preferences', () => {
const overrides: any = {
app: {
locale: 'en-US',
themeMode: 'light',
},
};
await preferenceManager.initPreferences({
@@ -79,10 +78,12 @@ describe('preferences', () => {
it('updates theme mode correctly', () => {
preferenceManager.updatePreferences({
app: { themeMode: 'light' },
theme: {
mode: 'light',
},
});
expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
it('updates color modes correctly', () => {
@@ -97,7 +98,9 @@ describe('preferences', () => {
it('resets preferences to default', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
app: { themeMode: 'light' },
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
@@ -146,10 +149,10 @@ describe('preferences', () => {
});
it('updates the sidebar collapse state correctly', () => {
preferenceManager.updatePreferences({
sidebar: { collapse: true },
sidebar: { collapsed: true },
});
expect(preferenceManager.getPreferences().sidebar.collapse).toBe(true);
expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
});
it('updates the navigation style type correctly', () => {
preferenceManager.updatePreferences({
@@ -164,8 +167,11 @@ describe('preferences', () => {
it('resets preferences to default correctly', () => {
// 先更新一些偏好设置
preferenceManager.updatePreferences({
app: { locale: 'en-US', themeMode: 'light' },
sidebar: { collapse: true, width: 200 },
app: { locale: 'en-US' },
sidebar: { collapsed: true, width: 200 },
theme: {
mode: 'light',
},
});
// 然后重置偏好设置
@@ -232,10 +238,10 @@ describe('preferences', () => {
await preferenceManager.initPreferences(overrides);
preferenceManager.updatePreferences({
app: { themeMode: 'light' },
theme: { mode: 'light' },
});
expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
});
});

View File

@@ -5,16 +5,17 @@ import type { Preferences } from './types';
import { markRaw, reactive, readonly, watch } from 'vue';
import { StorageManager } from '@vben-core/cache';
import { convertToHslCssVar, merge } from '@vben-core/toolkit';
import { generatorColorVariables } from '@vben-core/colorful';
import { merge, updateCSSVariables } from '@vben-core/toolkit';
import {
breakpointsTailwind,
useBreakpoints,
useCssVar,
useDebounceFn,
} from '@vueuse/core';
import { defaultPreferences } from './config';
import { BUILT_IN_THEME_PRESETS } from './constants';
const STORAGE_KEY = 'preferences';
const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
@@ -59,7 +60,7 @@ class PreferenceManager {
private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference);
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
this.cache?.setItem(STORAGE_KEY_THEME, preference.app.themeMode);
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
}
/**
@@ -72,11 +73,7 @@ class PreferenceManager {
const themeUpdates = updates.theme || {};
const appUpdates = updates.app || {};
if (themeUpdates.colorPrimary) {
this.updateCssVar(this.state);
}
if (appUpdates.themeMode) {
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
this.updateTheme(this.state);
}
@@ -149,7 +146,7 @@ class PreferenceManager {
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
this.updatePreferences({
app: { themeMode: isDark ? 'dark' : 'light' },
theme: { mode: isDark ? 'dark' : 'light' },
});
this.updateTheme(this.state);
});
@@ -178,15 +175,37 @@ class PreferenceManager {
* 更新 CSS 变量
* @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
*/
private updateCssVar(preference: Preferences) {
if (preference.theme) {
for (const [key, value] of Object.entries(preference.theme)) {
if (['colorPrimary'].includes(key)) {
const cssVarValue = useCssVar(`--primary`);
cssVarValue.value = convertToHslCssVar(value);
}
}
private updateMainColors(preference: Preferences) {
if (!preference.theme) {
return;
}
const { colorDestructive, colorPrimary, colorSuccess, colorWarning } =
preference.theme;
const colorVariables = generatorColorVariables([
{ color: colorPrimary, name: 'primary' },
{ alias: 'warning', color: colorWarning, name: 'yellow' },
{ alias: 'success', color: colorSuccess, name: 'green' },
{ alias: 'destructive', color: colorDestructive, name: 'red' },
]);
if (colorPrimary) {
document.documentElement.style.setProperty(
'--primary',
colorVariables['--primary-600'],
);
}
if (colorVariables['--green-600']) {
colorVariables['--success'] = colorVariables['--green-600'];
}
if (colorVariables['--yellow-600']) {
colorVariables['--warning'] = colorVariables['--yellow-600'];
}
if (colorVariables['--red-600']) {
colorVariables['--destructive'] = colorVariables['--red-600'];
}
updateCSSVariables(colorVariables);
}
/**
@@ -206,14 +225,61 @@ class PreferenceManager {
private updateTheme(preferences: Preferences) {
// 当修改到颜色变量时,更新 css 变量
const root = document.documentElement;
if (root) {
const themeMode = preferences?.app?.themeMode;
if (!themeMode) {
return;
}
const dark = isDarkTheme(themeMode);
if (!root) {
return;
}
const {
builtinType,
colorDestructive,
colorPrimary,
colorSuccess,
colorWarning,
mode,
radius,
} = preferences?.theme ?? {};
if (mode) {
const dark = isDarkTheme(mode);
root.classList.toggle('dark', dark);
}
if (builtinType) {
const rootTheme = root.dataset.theme;
if (rootTheme !== builtinType) {
root.dataset.theme = builtinType;
}
}
const currentBuiltType = BUILT_IN_THEME_PRESETS.find(
(item) => item.type === builtinType,
);
let builtinTypeColorPrimary: string | undefined = '';
if (currentBuiltType) {
const isDark = isDarkTheme(this.state.theme.mode);
const color = isDark
? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor
: currentBuiltType.primaryColor;
builtinTypeColorPrimary = color || currentBuiltType.color;
}
if (
builtinTypeColorPrimary ||
colorPrimary ||
colorDestructive ||
colorSuccess ||
colorWarning
) {
preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary;
this.updateMainColors(preferences);
}
if (radius) {
document.documentElement.style.setProperty('--radius', `${radius}rem`);
}
}
// public getFlatPreferences() {

View File

@@ -1,4 +1,5 @@
import type {
BuiltinThemeType,
ContentCompactType,
LayoutHeaderModeType,
LayoutType,
@@ -45,8 +46,6 @@ interface AppPreferences {
semiDarkMenu: boolean;
/** 是否显示偏好设置 */
showPreference: boolean;
/** 当前主题 */
themeMode: ThemeModeType;
}
interface BreadcrumbPreferences {
@@ -132,8 +131,20 @@ interface TabbarPreferences {
}
interface ThemePreferences {
/** 内置主题名 */
builtinType: BuiltinThemeType;
/** 错误色 */
colorDestructive: string;
/** 主题色 */
colorPrimary: string;
/** 成功色 */
colorSuccess: string;
/** 警告色 */
colorWarning: string;
/** 当前主题 */
mode: ThemeModeType;
/** 圆角 */
radius: string;
}
interface TransitionPreferences {

View File

@@ -24,7 +24,7 @@ function usePreferences() {
* @returns 如果主题为暗黑模式,返回 true否则返回 false。
*/
const isDark = computed(() => {
return isDarkTheme(appPreferences.value.themeMode);
return isDarkTheme(preferences.theme.mode);
});
const theme = computed(() => {