chore: Optimize multi-theme switching
This commit is contained in:
@@ -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",
|
||||
|
@@ -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,
|
||||
|
@@ -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 };
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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 {
|
||||
|
@@ -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(() => {
|
||||
|
Reference in New Issue
Block a user